Compare commits

..

496 Commits

Author SHA1 Message Date
b0c3b79e11
ticket: close installed binary rename 2026-05-29 09:39:09 +09:00
fa4130ab68
merge: rename installed binaries 2026-05-29 09:39:08 +09:00
372490707a
chore: use static crate fetch for nix vendor 2026-05-29 09:28:49 +09:00
1e8c11c564
fix: rename installed binaries 2026-05-29 09:28:31 +09:00
e7b89a169c
ticket: add installed binary rename 2026-05-29 09:14:07 +09:00
aab7af7e9c
ticket: close memory tool guidance prompt 2026-05-29 08:59:07 +09:00
6e89d6017b
merge: memory tool guidance prompt 2026-05-29 08:58:47 +09:00
b8609c35b6
ticket: close multi-pod open return 2026-05-29 08:57:50 +09:00
d79b5d5cc4
merge: multi-pod open return 2026-05-29 08:57:24 +09:00
775a4605cd
prompt: add memory tool usage guidance 2026-05-29 08:49:24 +09:00
96407899f7
tui: return to multi dashboard after opening pod 2026-05-29 08:45:15 +09:00
f73cfdc6e7
ticket: add multi-pod open return 2026-05-29 08:36:02 +09:00
7880672737
ticket: close multi-pod layout polish 2026-05-29 01:49:26 +09:00
c6d9b7f405
merge: multi-pod view section layout 2026-05-29 01:49:06 +09:00
2bb69ae7f6
tui: section multi-pod list layout 2026-05-29 01:46:48 +09:00
cbb4c4dec4
ticket: close nix packaging 2026-05-29 01:42:09 +09:00
420e83bea3
merge: nix packaging 2026-05-29 01:41:54 +09:00
7ecd58814c
nix: exclude local worktrees from package source 2026-05-29 01:41:09 +09:00
a8e9a091f8
ticket: add multi-pod layout polish 2026-05-29 01:33:35 +09:00
9df9dc1863
nix: add installable package 2026-05-29 01:32:04 +09:00
3c086b7497
ticket: close multi-pod TUI view 2026-05-29 01:09:02 +09:00
20f55a3c61
merge: multi-pod TUI view 2026-05-29 01:08:42 +09:00
7cfa5503df
feat: add multi pod tui dashboard 2026-05-29 01:04:56 +09:00
32be379f54
ticket: specify nix package file 2026-05-29 00:56:04 +09:00
8b2f16e009
ticket: specify multi-pod TUI entrypoint 2026-05-29 00:53:33 +09:00
3784cc8bbf
ticket: close TUI pod list abstraction 2026-05-29 00:40:32 +09:00
3457167931
merge: TUI pod list abstraction 2026-05-29 00:39:57 +09:00
d9984f33c2
tui: drain initial pod status events 2026-05-29 00:39:00 +09:00
35b13a98df
tui: add pod list model 2026-05-29 00:33:57 +09:00
74f792da1a
ticket: add web tools and nix packaging 2026-05-29 00:31:09 +09:00
26d8a5d9be
ticket: refine TUI pod list abstraction 2026-05-29 00:25:03 +09:00
41ce27f038
ticket: define multi-pod TUI view 2026-05-28 23:48:39 +09:00
f7d4b12e7f
ticket: add TUI pod list abstraction 2026-05-28 23:17:16 +09:00
6083121574
audit: record crate boundary findings 2026-05-28 22:25:54 +09:00
92a9c1416c
ticket: close spawnpod initial run confirmation 2026-05-28 22:25:28 +09:00
3cb1138e84
merge: spawnpod initial run confirmation 2026-05-28 22:24:14 +09:00
2b4bdda89c
fix: confirm initial SpawnPod run delivery 2026-05-28 22:14:28 +09:00
834d21723b
ticket: add crate boundary audit 2026-05-28 22:13:45 +09:00
3658242bbc
ticket: refine spawnpod socket delivery 2026-05-28 22:06:47 +09:00
eda4c4ce47
ticket: close compact session-log exploration 2026-05-28 18:53:52 +09:00
7265e83e44
test: fix runtime dir expectation 2026-05-28 18:53:52 +09:00
22df14a66c
merge: compact session-log exploration 2026-05-28 12:40:37 +09:00
0e4ab1d496
style: format manifest paths test 2026-05-28 12:38:53 +09:00
4450e1da9d
style: revert unrelated manifest path formatting 2026-05-28 12:36:32 +09:00
5d104a1cc6
merge: main trace diagnostics 2026-05-28 12:32:24 +09:00
b2efd2906f
feat: add compact session exploration tools 2026-05-28 12:31:44 +09:00
98522911a4
trace: llm stream open diagnostics 2026-05-28 12:26:14 +09:00
01c41ae86c
feat: bound compact worker context 2026-05-28 11:59:41 +09:00
9fe2799732
ticket: add compact work item metadata 2026-05-28 10:01:28 +09:00
2c3eddd218
ticket: compact session-log exploration 2026-05-28 10:01:03 +09:00
365b8c34fd sanitize: neutralize provider notes and remove claude knowledge 2026-05-28 07:45:49 +09:00
4361385946 sanitize: remove local path references from current tree 2026-05-28 06:26:34 +09:00
9ccbdda27c chore: record spawnpod hang report and local manifest 2026-05-28 06:21:01 +09:00
9a0ef7c799 work-items: close openai unhandled sse observability 2026-05-28 05:44:20 +09:00
732d6a57b7 merge: openai unhandled sse observability 2026-05-28 05:44:14 +09:00
3b90f26dea fix: trace unhandled openai responses sse 2026-05-28 05:18:57 +09:00
6877447616 work-items: add openai unhandled sse observability 2026-05-28 05:13:41 +09:00
b808811843 work-items: add pod orchestration guidance item 2026-05-28 04:45:03 +09:00
838ccbb65f work-items: close tickets sh mvp 2026-05-28 04:29:35 +09:00
4d080ca985 merge: tickets work item thread mvp 2026-05-28 04:27:56 +09:00
b1d8f7f181 fix: repair migrated work item encoding 2026-05-28 04:09:47 +09:00
134d0ce2a1 feat: add tickets work item mvp 2026-05-28 03:59:05 +09:00
2820cbbe53 ticket: clarify workitem migration scope 2026-05-28 03:49:21 +09:00
5c6df298aa ticket: complete openai responses diagnostics 2026-05-28 03:23:54 +09:00
ae196c2a87 ticket: record openai responses diagnostics fix 2026-05-28 03:23:25 +09:00
f56793589f fix: preserve openai responses incomplete diagnostics 2026-05-28 03:22:53 +09:00
33884bd0ce ticket: complete memory consolidation skip observability 2026-05-28 03:09:42 +09:00
2ba35cca23 merge: memory consolidation skip observability 2026-05-28 03:09:14 +09:00
860767a143 ticket: complete llm request timeout fix 2026-05-28 02:44:00 +09:00
d65cfe146d ticket: record llm request timeout fix 2026-05-28 02:43:23 +09:00
1babd021b0 fix: add llm request lifecycle timeouts 2026-05-28 02:42:31 +09:00
bdabe789e3 ticket: openai responses incomplete observability 2026-05-28 02:40:30 +09:00
48c4c9b56b ticket: llm client request timeouts 2026-05-28 02:07:01 +09:00
b1fb3ec0fa ticket: complete codex oauth wire compatibility 2026-05-28 02:05:49 +09:00
b3c739867e fix: align codex oauth wire behavior 2026-05-28 01:57:04 +09:00
5ae886ea99 ticket: codex oauth wire compatibility 2026-05-28 01:44:30 +09:00
2c67f99054 fix: suppress memory idle skip notices 2026-05-27 18:55:58 +09:00
67b5d6354c ticket: complete compact retained split fix 2026-05-26 21:40:18 +09:00
cdc42e5a86 ticket: record compact retained split fix 2026-05-26 21:39:57 +09:00
e49817c2d5 feat: trace pre-stream lifecycle 2026-05-26 21:05:45 +09:00
9405ffc633 feat: add session stream event trace flag 2026-05-26 19:57:47 +09:00
77e2ad0c40 fix: compact retained split uses raw tail size 2026-05-26 17:52:09 +09:00
a2771180cc ticket: compact retained split usage records 2026-05-26 17:04:29 +09:00
2cfc3b63c2 ticket: pod scope persistence authority 2026-05-26 16:50:01 +09:00
b2e53f2f61 chore: complete memory summary resident injection ticket 2026-05-26 13:29:03 +09:00
b22040ac84 chore: complete tui user manifest env overlay ticket 2026-05-26 10:10:00 +09:00
80a4f90004 fix: align spawn user manifest env overlay 2026-05-26 10:09:17 +09:00
0b582faebc merge: memory summary resident injection 2026-05-26 09:55:24 +09:00
d084923878 fix: split resident injection gates 2026-05-26 09:44:24 +09:00
df3373c3f2 docs: add tickets.sh workitem mvp ticket 2026-05-26 09:33:30 +09:00
a3e852c6b3 docs: add memory tool guidance ticket 2026-05-26 09:21:57 +09:00
b25f4c7468 feat: inject memory summary into resident prompt 2026-05-26 09:21:10 +09:00
39d40d391b chore: tune project memory thresholds 2026-05-26 09:05:14 +09:00
f87cf5bd00 docs: add memory summary resident injection ticket 2026-05-26 08:50:58 +09:00
9f5e27f3fd merge: memory consolidation skip observability 2026-05-26 08:37:32 +09:00
c101b42619 fix: confirm SpawnPod initial run delivery 2026-05-26 08:37:24 +09:00
f56ef010a8 chore: ignore generated insomnia memory 2026-05-26 08:14:46 +09:00
8095c86be2 fix: suppress memory idle skip notices 2026-05-26 08:03:17 +09:00
99797b9e40 docs: refine memory consolidation skip ticket 2026-05-26 07:53:37 +09:00
1ac197fc6c chore: complete llm retry continuation ticket 2026-05-26 07:22:45 +09:00
3ff78c03af feat: surface llm retry and continuation state 2026-05-26 07:13:59 +09:00
156a55d1d1 docs: refine llm retry continuation ticket 2026-05-26 05:20:43 +09:00
597c6fc3e9 docs: note spawnpod delivery race precedent 2026-05-25 07:03:00 +09:00
a70fe65ed5 docs: add spawnpod run delivery ticket 2026-05-25 06:37:38 +09:00
fa225eb01d docs: add live pending pod picker ticket 2026-05-25 06:29:13 +09:00
8e21e2f3f2 docs: add memory consolidation skip ticket 2026-05-25 05:43:06 +09:00
f51f17cf93 docs: specify stream continuation policy 2026-05-25 04:48:07 +09:00
7e4d90fc1b chore: complete memory audit log ticket 2026-05-25 03:38:18 +09:00
235ddba9c5 merge: memory-audit-log 2026-05-25 03:38:03 +09:00
fe6f5eb326 memory: add audit log events 2026-05-25 03:24:04 +09:00
06da8c5b00 docs: add actionbar notice api ticket 2026-05-25 02:40:59 +09:00
87b2e8eb16 docs: expand memory audit log ticket 2026-05-25 02:06:42 +09:00
2f3adc3d14 fix: refine command mode footer 2026-05-25 01:08:41 +09:00
21ec057de0 chore: complete tui-system-command-compact ticket 2026-05-24 09:40:41 +09:00
dd571a963e merge: tui-system-command-compact 2026-05-24 09:40:25 +09:00
afd65442c5 test: clean up compact event assertion 2026-05-24 09:39:57 +09:00
a4358eed14 feat: add manual compact command 2026-05-24 08:59:44 +09:00
9685bfffba chore: complete tui-command-mode ticket 2026-05-24 08:39:25 +09:00
3a734c30bf merge: tui-command-mode 2026-05-24 08:38:39 +09:00
6e8aa92e38 feat: add TUI command mode 2026-05-24 08:32:21 +09:00
811a449c28 docs: replace gui mvp with tui spawned pod panel 2026-05-24 08:10:21 +09:00
0fd995c85e docs: split tui command and navigation tickets 2026-05-24 07:59:51 +09:00
e0d7468ebb chore: complete worker-history-append-contract ticket 2026-05-24 07:37:29 +09:00
07dc185032 merge: worker-history-append-contract 2026-05-24 07:37:05 +09:00
efb0ac7da3 docs: split maintainer workflows by role 2026-05-24 07:34:30 +09:00
e7b0a0b20f fix: route worker history appends through callbacks 2026-05-24 06:44:19 +09:00
5508299e76 chore: drop stale tui spawn error todo 2026-05-24 06:29:15 +09:00
59e4aac7f7 chore: complete tui-input-queue ticket 2026-05-23 13:58:09 +09:00
6485632a4c merge: tui-input-queue 2026-05-23 13:57:32 +09:00
8d6b47bef1 feat: queue tui input during runs 2026-05-23 13:57:22 +09:00
6046842242 docs: add manual turn rollback ticket 2026-05-23 13:35:03 +09:00
1c8b349e01 chore: complete tui-empty-turn-restore ticket 2026-05-23 13:30:01 +09:00
d70c10b782 merge: tui-empty-turn-restore 2026-05-23 13:29:07 +09:00
70c0548190 feat: restore rolled back tui input 2026-05-23 13:28:56 +09:00
6acaccccf7 chore: complete pod-empty-turn-rollback ticket 2026-05-23 12:52:42 +09:00
b9dd0ba0d0 merge: pod-empty-turn-rollback 2026-05-23 12:52:12 +09:00
23e218abaa chore: handle rolled back run result clients 2026-05-23 12:51:40 +09:00
55dedd173c feat: rollback empty interrupted turns 2026-05-23 12:50:46 +09:00
df629b4dc6 fix: make visible pod list schema object 2026-05-23 12:29:37 +09:00
7c573f36e2 chore: complete pod-discovery-restore-tools ticket 2026-05-23 12:05:30 +09:00
ca869195dc merge: pod-discovery-restore-tools 2026-05-23 12:04:59 +09:00
e6fa660a5f feat: add visible pod discovery tools 2026-05-23 12:04:45 +09:00
f7a3b0adf1 chore: complete memory-extract-remove-input-cap ticket 2026-05-23 09:14:37 +09:00
ea9e924d35 merge: memory-extract-remove-input-cap 2026-05-23 09:14:15 +09:00
3b582a4f73 fix: remove memory extract input cap 2026-05-23 09:14:07 +09:00
2a721e3776 chore: complete tui-pod-restore-picker ticket 2026-05-23 09:13:57 +09:00
61347362d1 merge: tui-pod-restore-picker 2026-05-23 09:13:19 +09:00
e2688da828 feat: restore tui sessions by pod 2026-05-23 09:13:06 +09:00
a0e544e3e4 chore: complete spawned-delegation-scope-reclaim ticket 2026-05-23 08:39:04 +09:00
96fd9574a2 merge: spawned-delegation-scope-reclaim 2026-05-23 08:38:50 +09:00
ab4611001e fix: reclaim delegated scope from stopped children 2026-05-23 08:38:42 +09:00
5b20d21ea0 docs: refine pod visibility and tui restore flow 2026-05-23 08:33:00 +09:00
48625f5077 update: tui -rの際のリストの時系列ソート 2026-05-23 08:02:05 +09:00
fbe8846393 chore: complete tui-streaming-input-loss ticket 2026-05-23 07:16:08 +09:00
8ae3849cc8 merge: tui-streaming-input-loss 2026-05-23 07:15:55 +09:00
e29861f787 fix: preserve tui input during streaming 2026-05-23 07:15:39 +09:00
fa00c1f188 chore: complete tui-context-usage-indicator ticket 2026-05-23 07:15:30 +09:00
879434e240 merge: tui-context-usage-indicator 2026-05-23 07:15:17 +09:00
4b263f8743 feat: show context usage in tui status 2026-05-23 07:15:03 +09:00
7315114b20 docs: identify tui streaming input loss race 2026-05-23 05:47:59 +09:00
f14c8cb614 Create tui-parts.md 2026-05-23 05:41:48 +09:00
da5d789897 fix: tighten task tool usage guidance 2026-05-23 05:11:48 +09:00
802cbf2f45 chore: complete prune-token-budget ticket 2026-05-23 05:00:30 +09:00
18c30c5f90 merge: prune-token-budget 2026-05-23 05:00:15 +09:00
dfec60438e feat: protect prune tail by token budget 2026-05-23 05:00:06 +09:00
6a5b8ed152 chore: complete pod-event-callback-delivery ticket 2026-05-23 04:57:26 +09:00
baaec0c77f merge: pod-event-callback-delivery 2026-05-23 04:57:10 +09:00
fdd2f16df0 fix: drain snapshots before pod callbacks 2026-05-23 04:57:03 +09:00
3e7a15a2b5 docs: add memory extract input cap ticket 2026-05-23 04:42:38 +09:00
b5219dc862 docs: add pod event callback delivery ticket 2026-05-23 03:29:01 +09:00
c1173dd8a1 docs: add spawned delegation scope reclaim ticket 2026-05-23 03:02:48 +09:00
f03e84a62a refactor: remove legacy plural log entries 2026-05-23 02:03:42 +09:00
e80a3fbf8e docs: track read pod output log entry bug 2026-05-23 00:53:47 +09:00
8947a89e7b docs: add pod discovery restore tools ticket 2026-05-23 00:09:34 +09:00
f46cdd6dbc chore: complete spawned-registry-persist ticket 2026-05-22 23:30:16 +09:00
1a5b5331d6 merge: spawned-registry-persist 2026-05-22 23:30:06 +09:00
530027c62b feat: persist spawned pod registry 2026-05-22 23:30:02 +09:00
8e7126d177 chore: complete pod-name-resume ticket 2026-05-22 22:57:31 +09:00
3fe4a6bc14 merge: pod-name-resume 2026-05-22 22:57:23 +09:00
12a4ba5edf feat: resume pods by name 2026-05-22 22:57:16 +09:00
d3b78234c2 chore: complete pod-state-write-points ticket 2026-05-22 22:29:23 +09:00
baf7403c8c merge: pod-state-write-points 2026-05-22 22:29:12 +09:00
5955695db8 feat: wire pod metadata lifecycle writes 2026-05-22 22:29:08 +09:00
bacba69d31 chore: complete pod-state-backend ticket 2026-05-22 22:03:36 +09:00
08dc6b29f8 style: run cargo fmt 2026-05-22 22:03:27 +09:00
d08ea1734e merge: pod-state-backend 2026-05-22 22:03:17 +09:00
ec5b891fec feat: add pod metadata store backend 2026-05-22 22:03:11 +09:00
5830bb9c85 Merge: live-fork-marker 2026-05-20 06:45:49 +09:00
16ef135f1f chore: 空になった Storage 親見出しを TODO から削除 2026-05-20 06:45:43 +09:00
15f514dfe2 ticket: live-fork-marker 完了 2026-05-20 06:45:19 +09:00
fbd97c3546 chore: auto-fork ロジック二重実装を KNOWN_ISSUES に登録 2026-05-20 06:45:14 +09:00
bb4205b531 ticket: live-fork-marker レビュー (Approve) 2026-05-20 06:44:54 +09:00
077efee13b feat: live auto-fork の marker 形式を確定(seq 比較 + forked_from 記録)
方針: 末尾 entry-count 比較で検知し、元 Segment は immutable のまま
(terminal marker を書き戻さない)。fork lineage は新 Segment の
SegmentStart.forked_from に前向きに記録するため、log だけから辿れる。
過去 fork と対称で、nested fork も marker 位置の調停が不要。

- session-store ensure_head_or_fork に at_turn_index 引数を追加し
  新 Segment へ forked_from を記録
- pod ensure_segment_head の auto-fork も同様に forked_from を記録
  (at_turn_index = writer の現 turn_count)
- fork_at の doc に「元 Segment を mutate しない」invariant を明記
- test: nested past-fork が祖先を不変に保つ / Pod 並行 writer drift で
  auto-fork し forked_from を記録 / 元 Segment に marker が書かれない
2026-05-20 06:42:09 +09:00
b5d5c03412 Merge: session-grouping-introduce 2026-05-20 06:29:48 +09:00
bee41379fa ticket: session-grouping-introduce 完了 2026-05-20 06:29:43 +09:00
842e7a3c58 update: session-grouping review follow-up
- PickerOutcome::Picked から未使用の session_id を除去(pod-cli が lookup_session_of で再解決)
- picker preview が singular AssistantItem も拾うように
- fs_store layout doc に migration(後方互換なし、旧 flat sessions は破棄)を明記
- TaskStore は Session-lifetime、ScopedFs/Tracker は Pod-process lifetime と用語整理
- Pod::session_id / from_manifest_spawned のコメント補強
2026-05-20 06:29:37 +09:00
e8c16be475 feat: Session(Segment 群の grouping)を導入
- SessionId 型を新設、各 SegmentStart に session_id を持たせる
- compaction / 内部 fork は同 SessionId を継承、fork() は新 Session を発行
- Store API を (SessionId, SegmentId) ベースに、FsStore layout は
  <root>/<session_id>/<segment_id>.jsonl に
- Store::list_sessions / list_segments(session_id) / lookup_session_of を追加
- restore_by_segment shim を session-store に提供(pod-cli --session で使用)
- SegmentState に SegmentLocation (session_id, segment_id) を保持し ArcSwap で更新
- RestoredState に session_id: Option<SessionId> を追加
- Picker は Session 単位に列挙、leaf segment を解決して resume
2026-05-20 06:17:56 +09:00
58f54b99f3 Merge: segment-rename 2026-05-20 05:18:11 +09:00
5aea9730c6 ticket: segment-rename 完了 2026-05-20 05:18:04 +09:00
a63f076856 update: 残存 Session 識別子の Segment 化(review follow-up)
レビュー指摘の通り、次の session-grouping-introduce で新 SessionId が
入る前に名称衝突を避けるため取り残しを掃除。

- PodError::Session{Empty,ScopeMissing} → Segment{Empty,ScopeMissing}
- ScopeLockError::SessionConflict → SegmentConflict
- Pod.session_state / SegmentState.set_session_id 系
- source_session_id / prev_session_id / ensure_session_head / short_session
- pod_cli の "Session ID:" 表示
- fs_store の sessions ローカル変数
2026-05-20 05:17:49 +09:00
ac1d8b1c7d update: Session-lifetime/scoped を Pod-lifetime に修正
タスクストア/ファイルトラッカーは compaction を跨いで Pod プロセス寿命まで生きる。
旧 SessionId = Segment の時代の表現を Pod-lifetime に正す。pod_cli の表示も Segment: に。
2026-05-20 05:06:38 +09:00
d5fcbc2125 update: SessionId / SessionStart / SessionOrigin 等を Segment 系名称へ
- Type/Function/Variantを Segment* 系へ統一
  - SessionId/SessionStart/SessionOrigin/SessionStartState/SessionState/SessionLogSink/SessionLockInfo
  - new_session_id / session_id / create_session* / list_sessions / lookup_session / update_session / find_by_session
  - protocol Event::SessionRotated → SegmentRotated、CompactDone.new_session_id → new_segment_id
- Module: session_log → segment_log / session → segment (file mv 含む)
  pod 側の session_log_sink → segment_log_sink も同様
- crate 名 (session-store)、CLI flag (--session)、ResumeWithSession (CLI tied) は据え置き
- session-tests/session_metrics_test 等の Store impl も追従
2026-05-20 05:06:04 +09:00
45db480b0b Merge: entry-hash-abolish 2026-05-20 04:53:52 +09:00
4b8aee909b ticket: entry-hash-abolish 完了 2026-05-20 04:53:47 +09:00
903cfa3060 update: 旧用語コメントの掃除と KNOWN_ISSUES 追記
- 残存していた head_hash / SessionHead 言及コメントを 3 箇所更新
- FsStore::read_entry_count の O(n) 計測コストを KNOWN_ISSUES に登録
2026-05-20 04:53:33 +09:00
27a1d07e98 ticket: entry-hash-abolish レビュー (Approve) 2026-05-20 04:49:17 +09:00
9bfbb2fb4c update: entry hash chain と session_head mutex を撤廃
- HashedEntry / EntryHash / compute_hash / build_chain 撤去、JSONL は 1 行 1 LogEntry
- SessionOrigin.at_hash → at_turn_index (TurnEnd 由来) に置換
- Pod 側 SessionHead mutex を ArcSwap<SessionId> + AtomicUsize の SessionState に置換
- ensure_head_or_fork は store の entry count と writer の append tally で判定
- session-store から sha2 / hex 依存、pod から parking_lot 依存を削除
2026-05-20 04:31:37 +09:00
1a9bb30824 ticket: 永続化整理を 8 個に分割
persistence-semantics と pod-persistent-state を実装可能な粒度に分割。
Storage 層 (Phase 1) を entry-hash-abolish / segment-rename /
session-grouping-introduce / live-fork-marker に、Pod 単位永続化
(Phase 2) を pod-state-backend / pod-state-write-points /
pod-name-resume / spawned-registry-persist に切り出した。
2026-05-20 04:07:44 +09:00
0440d5c6dc Merge: invoke-turn-llmcall-semantics
# Conflicts:
#	crates/pod/src/controller.rs
2026-05-15 22:08:41 +09:00
01200a0d33 ticket: invoke-turn-llmcall-semantics 完了 2026-05-15 21:54:40 +09:00
d3b7663d41 ticket: worker-history-append-contract 作成 2026-05-15 21:53:24 +09:00
b204909c4c chore: KNOWN_ISSUES に controller_test::double_run_returns_error の flakiness を追記 2026-05-15 21:52:40 +09:00
9a89d2419a ticket: pod-interrupt-prep-internalize 完了 2026-05-15 21:52:24 +09:00
af6427ff67 ticket: pod-interrupt-prep-internalize レビュー (Approve with follow-up) 2026-05-15 21:51:57 +09:00
4c8596db38 update: Paused→Run の interrupt 前処理を Pod::run に内包 2026-05-15 21:51:57 +09:00
c779768b6e ticket: invoke-turn-llmcall-semantics review (Approve) 2026-05-15 21:42:43 +09:00
49b78612d6 feat: Invoke marker と LlmCall callback を導入し AgentTurn セマンティクスを明確化
- protocol: InvokeKind enum、Event::InvokeStart / LlmCallStart / LlmCallEnd 追加
- llm-worker: Worker.llm_call_count と on_llm_call_start/end callback、turn_count を AgentTurn 数として doc 更新
- session-store: LogEntry::Invoke { ts, trigger } 追加 (replay は marker のみで state 不変)
- pod: run/run_for_notification 開始時に Invoke marker commit、PendingRun::RunForNotification(InvokeKind) で kind を伝搬
- pod ipc: sink + server で Invoke エントリーを Event::InvokeStart として broadcast
- tui: 新 Event 3種を no-op で受理 (UI 設計はチケット範囲外)
2026-05-15 07:04:26 +09:00
ce6085b5f4 ticket: invoke/turn/llmcall 決定事項と実装範囲を明文化 2026-05-15 06:48:57 +09:00
1b83b2c40a ticket: Exchange語撤廃、Invoke/Turn/LlmCall でセマンティクスを再整理 2026-05-15 05:41:13 +09:00
9d04008123 ticket: pod-input-validate-internlize完了 2026-05-15 05:38:27 +09:00
4ebc2c96b3 update: Controllerで入力のValidationを行っていた部分をPod側に移す 2026-05-15 05:33:33 +09:00
bb6f7e2022 ticket: PodとControllerの責務の抱え違いを修正するチケット 2026-05-15 04:52:39 +09:00
86b48a9fdf ticket: pod-parent-turn-callback完了 2026-05-15 04:43:12 +09:00
59067bd115 ticket: pod-parent-turn-callbackレビュー 2026-05-15 04:42:29 +09:00
6116d72570 ticket: 消し忘れ 2026-05-15 04:39:30 +09:00
8e8c0887de update: 親にターン完了を通達する経路の整理 2026-05-15 04:38:53 +09:00
3143353ddc update: エントリの単数化のフォローアップ 2026-05-14 19:42:23 +09:00
f35d99900f update: 書き込みの不要なasyncを削除 2026-05-14 19:16:48 +09:00
6e7494553b ticket: 書き込みのsync化を計画 2026-05-14 16:45:58 +09:00
904ea6e326 update: SystemItem1本化 2026-05-14 14:36:29 +09:00
b6b158a244 ticket: イベントプロトコルと永続化におけるシステムイベントの統合 2026-05-14 04:12:40 +09:00
e32b210d50 chore: cargo fmt 2026-05-14 03:36:08 +09:00
a02f34437c fix: 実態にそぐわないEvent::Entryを実装した構造を訂正 2026-05-14 03:35:52 +09:00
1ef094f039 refactor: Podのメインループのリファクタリング 2026-05-14 03:27:49 +09:00
e57e23b999 ticket: 追加:Podのメインループとソケット通信周りのリファクタリング 2026-05-13 22:16:25 +09:00
13feb36518 ticket: add tui manual compact command 2026-05-13 06:50:27 +09:00
9e4bdf315f docs: update pod cli manifest flags 2026-05-13 06:44:48 +09:00
068a975488 ticket: note tui user manifest overlay mismatch 2026-05-13 06:41:23 +09:00
3d23c4ed40 close: complete pod manifest and file ref tickets 2026-05-13 06:30:45 +09:00
d2149d11d3 merge: file-ref-directory 2026-05-13 06:30:45 +09:00
ada2988105 merge: pod-cli-manifest-flags 2026-05-13 06:30:45 +09:00
d6cfea463a review: file-ref-directory 2026-05-13 06:30:45 +09:00
43330cf624 review: pod-cli-manifest-flags 2026-05-13 06:30:45 +09:00
21a78fb19e refactor: PodControllerの構造のリファクタリング 2026-05-13 06:07:38 +09:00
0ae6592032 docs(tickets): PodControllerの構造調整チケット作成 2026-05-13 05:43:23 +09:00
0e1539fefa chore: planの更新 2026-05-13 05:42:55 +09:00
dff72e291b feat: handle directory file refs 2026-05-13 02:57:58 +09:00
c6a9007b58 feat: organize pod manifest cli flags 2026-05-13 02:57:50 +09:00
d1c7297f87 feat: Languageインストラクションの追加 2026-05-13 02:27:30 +09:00
3c4a34b13b update: fmt + memoryに用いる言語の構成 2026-05-13 01:57:04 +09:00
076cf9af18 fix: compact時にToolCallとOutputの間でCutしてしまう問題 2026-05-13 00:59:02 +09:00
2f5f5b8a26 chore: workflowの調整・knowledgeの追加テスト 2026-05-13 00:06:33 +09:00
a363546a14 merge: lint common crate 2026-05-12 21:56:49 +09:00
599b24fa9e chore: complete lint common crate ticket 2026-05-12 21:56:39 +09:00
4bdbac6597 refactor: extract shared lint record primitives 2026-05-12 21:56:25 +09:00
20a6748cdd docs(tickets): submit時FileRefでディレクトリを参照した時の挙動 2026-05-12 17:39:40 +09:00
1271d13f26 docs(tickets): mainfest-output-upload-limits完了 2026-05-12 17:27:47 +09:00
5882341b21 feat: add manifest output upload limits 2026-05-12 16:20:15 +09:00
19730ba7c0 Merge branch 'tui-knowledge-completion' into develop 2026-05-12 15:43:29 +09:00
7a76276539 docs(memory): fix knowledge dir path in collect_resident_knowledge doc 2026-05-12 15:07:39 +09:00
64d12f2a6f docs(tickets): review tui knowledge completion (approve) 2026-05-12 14:56:30 +09:00
668bde46f4 feat(pod): wire knowledge slugs into # completion 2026-05-12 14:45:46 +09:00
3647614ab0 docs(tickets): tui knowledge completion unimplemented fix 2026-05-12 14:40:37 +09:00
5a2e69b2bf docs(tickets): define work item query strategy 2026-05-12 02:32:32 +09:00
1d53929250 docs(tickets): use timestamp work item ids 2026-05-12 02:07:29 +09:00
91a0a935b0 docs: add ai maintainer work item plan 2026-05-12 01:53:52 +09:00
bd46491b04 docs(tickets): add lint-common crate ticket 2026-05-12 00:06:06 +09:00
18b0f8b19f merge: workflow crate extraction 2026-05-11 22:50:19 +09:00
f5d69504b5 docs(tickets): complete workflow crate extraction 2026-05-11 22:50:06 +09:00
76e1287cbe review: workflow crate extraction 2026-05-11 22:49:50 +09:00
eb791f9e80 refactor: extract workflow crate 2026-05-11 22:49:07 +09:00
a1b9c865df merge: anthropic assistant burst bundling 2026-05-11 22:24:36 +09:00
985931d6fa docs(tickets): complete anthropic assistant burst bundling 2026-05-11 22:23:53 +09:00
d35c9f40a7 review: anthropic assistant burst bundling 2026-05-11 22:23:38 +09:00
4d6d5b631c fix: bundle anthropic assistant bursts 2026-05-11 22:22:36 +09:00
3354c41e66 merge: memory usage metrics 2026-05-11 21:46:24 +09:00
73d1c05edc docs(tickets): complete memory usage metrics 2026-05-11 21:46:19 +09:00
9e615a41f0 review: memory usage metrics 2026-05-11 21:46:19 +09:00
da4f4cc954 feat: add memory usage event metrics 2026-05-11 21:29:48 +09:00
01d38f042c docs(tickets): complete memory phase naming cleanup 2026-05-11 17:16:36 +09:00
9b99f50264 docs(tickets): simplify memory usage metrics 2026-05-11 16:54:23 +09:00
646b47b40f fix: remove remaining memory phase wording 2026-05-11 01:57:39 +09:00
5cf8eb94c7 docs(tickets): compact-worker-occupancy-cap完了 2026-05-11 01:56:20 +09:00
4d61d044ec update: memoryシステムの"Phase"表記を撤廃 2026-05-11 01:55:28 +09:00
967e57c933 docs(tickets): memory-extract-occupancy-cap 完了 2026-05-11 01:32:45 +09:00
acfe073b29 review: memory-extract-occupancy-cap (approve) 2026-05-11 01:25:20 +09:00
0b79e0ed65 feat: extract worker サーキットブレーカーを占有量ベースに統一 2026-05-11 01:20:37 +09:00
c8871ec4fe docs(tickets): add memory-extract-occupancy-cap ticket 2026-05-11 01:14:59 +09:00
3fece8749b Merge branch 'compact-worker-occupancy-cap' into develop 2026-05-11 01:12:32 +09:00
cac1f4d4fe review: compact-worker-occupancy-cap (set_max_turns 分岐削除) 2026-05-11 00:56:41 +09:00
e664def920 feat: compact worker サーキットブレーカーを占有量ベースに統一 2026-05-11 00:43:16 +09:00
f0a1f98912 docs(tickets): add memory audit log ticket 2026-05-11 00:06:42 +09:00
5ca771ded4 docs(tickets): completed tickets cleanup 2026-05-10 17:31:34 +09:00
9b15135416 merge: memory prompt record policy 2026-05-10 14:40:58 +09:00
b6f99b7651 docs: generalize memory prompt record policy 2026-05-10 14:40:52 +09:00
13c05b1083 docs: memory effectiveness plan 2026-05-10 01:25:10 +09:00
05da79f966 docs: memory prompt ticket policy ticket 2026-05-10 01:13:57 +09:00
92cee690f8 feat: client-crateの実装 2026-05-10 00:57:50 +09:00
6f0ec92f91 chore: E2Eの計画とgit運用の話 2026-05-09 05:04:57 +09:00
32ed5a812c docs(tickets): file-ref-symlink-diagnostics完了 2026-05-09 04:22:27 +09:00
856a0a2432 docs(tickets): file-ref-symlink-diagnosticsレビュー 2026-05-09 04:21:56 +09:00
ced26b952e feat: Toolsのシンボリックリンク対応 2026-05-09 04:21:56 +09:00
e451b07783 docs(tickets): tui-assistant-markdown完了 2026-05-09 03:31:49 +09:00
f6600feab5 docs(tickets): permission既定policy整理チケット追加 2026-05-09 03:27:22 +09:00
553d67a910 docs(tickets): permission-extension-point完了 2026-05-09 03:20:17 +09:00
805be47128 feat: パターンベースのツール権限制御を追加 2026-05-09 03:20:02 +09:00
aa9409869e chore: tui compact progress ticket完了 2026-05-09 03:14:23 +09:00
8ebdd47fbb feat: compactのプログレス表示 2026-05-09 03:11:53 +09:00
ec1eccd10d chore: git方針の変更とセマンティクス変更の計画の帳尻合わせ 2026-05-08 20:17:11 +09:00
42127554d4 docs(tickets): 自己改善workflowの設計 2026-05-08 01:50:55 +09:00
9dbfd15687 docs(tickets): workflow-directory-layout完了 2026-05-08 01:08:25 +09:00
6c31264377 update: Workflowディレクトリ修正のフォローアップ 2026-05-08 00:59:08 +09:00
b6b4168503 feat: Workflowの読み取り位置変更の実装 2026-05-08 00:15:50 +09:00
40cde699a8 docs(tickets): reportの運用・Workflowのディレクトリ位置修正 2026-05-07 23:34:00 +09:00
1ed45032be feat: TUIのmarkdown対応 2026-05-05 18:30:25 +09:00
64814c2e15 docs(tickets): PermissionのチケットとTUIのmd表示 2026-05-05 17:16:03 +09:00
96daebff30 docs(tickets): agent-skills完了 2026-05-05 16:00:40 +09:00
85fe1a094c update: Agent skills実装のレビュー・対応 2026-05-05 13:54:02 +09:00
68249b8072 feat: writingに対する基本的な指示promptを追加 2026-05-05 13:42:34 +09:00
98018972aa feat: agent skillsの互換実装 2026-05-05 13:16:10 +09:00
5b1324a630 fix: Reasoningの永続化のスキーマのミスを修正 2026-05-05 12:30:29 +09:00
4e352bb9ff docs(tickets): turnのセマンティクスを変える計画 2026-05-05 12:29:52 +09:00
5c8d00e49b docs(tickets): reasoning-history-perisit完了 2026-05-04 23:06:21 +09:00
94bb8804f4 update: Reasoningコンテキスト管理のレビュー・対応 2026-05-04 23:05:08 +09:00
30023349b9 feat: Reasoningのコンテキスト管理の対応 2026-05-04 21:31:44 +09:00
b0e6ab16b1 docs(tickets): Reasoningのコンテキスト管理とPruneの調整チケット追加 2026-05-04 21:16:31 +09:00
6e6be6f3ff docs(tickets): tui-task-display完了 2026-05-04 20:43:21 +09:00
eb9bd84b05 feat: Task表示のレビュー・修正 2026-05-04 17:28:39 +09:00
17a7744da1 feat: TUI上に進行中のTaskを表示する実装 2026-05-04 17:06:02 +09:00
a3082072d7 docs(tickets): Compaction進行中のライブ表示 2026-05-04 17:03:51 +09:00
04a471b669 docs(tickets): post-run memory detach 完了 2026-05-04 16:11:38 +09:00
3266ddb2d4 feat: Pos処理の非同期化・Busy状態の削除 2026-05-04 15:52:27 +09:00
7527b55de4 docs(tickets): 追加:タスクリストの表示とコンテキスト長インジケータ 2026-05-04 15:32:40 +09:00
c57d4be413 docs(tickets): Busyの切り離し 2026-05-04 13:20:25 +09:00
344dca6ffa Merge branch 'llm-worker-transient-retry' into develop 2026-05-04 13:16:26 +09:00
93fe2eb0ff docs(tickets): pod状態のTUI同期完了 2026-05-04 13:08:44 +09:00
09e465d583 feat: Podのステータス同期の修正 2026-05-04 12:55:29 +09:00
4eb73fa552 feat: Podのステータスを厳密にし、同期漏れを防ぐ 2026-05-04 12:55:11 +09:00
2d59ddd228 docs(tickets): llm-worker-transient-retry完了 2026-05-04 12:51:41 +09:00
39882263d3 docs(tickets): llm-worker-transient-retry レビュー追記
7183847 のレビュー結果を Approve として記録する。チケット要件
(リトライ対象 / バックオフ / Retry-After 上書き / mid-stream 温存 /
完了条件) はすべて満たしており、コードベースの層構造を歪める変更も
ない。Retry-After テストの方針差 (実時間 1s vs 仮想時間 5s) と
connect refused テストの試行回数未検証は non-blocking として
review.md に記録。
2026-05-04 12:49:13 +09:00
c2caaa21a0 feat(llm-worker): HTTP transient エラーへのリトライを追加
`transport.rs` の HTTP 送信〜ステータスチェック区間に指数バックオフ
+ フルジッターのリトライループを追加する。SSE 読み出し開始後 (
`bytes_stream()` 以降) のエラーは従来どおりそのまま流す。

- `is_retryable(&ClientError)`: 408/425/429/500/502/503/504/529 と
  reqwest の connect/timeout のみ true
- `RetryPolicy` (default: base 500ms / cap 10s / max_attempts 4 /
  total_timeout 30s)
- `Retry-After` ヘッダ (秒数) があればバックオフを上書き
- リトライ発火ごとに warn! でステータス・attempt・wait を出す

ref: tickets/llm-worker-transient-retry.md
2026-05-04 12:45:33 +09:00
20097e8296 Merge branch 'tui-system-message-render' into develop 2026-05-04 12:10:17 +09:00
185db7f8cd docs(tickets): tui-system-message-render完了 2026-05-04 12:05:50 +09:00
8870af800f feat: システムメッセージをTUIで表示させる 2026-05-04 12:04:09 +09:00
56f9bab7b7 update: Taskツールの説明を更新 2026-05-04 11:32:04 +09:00
194d29723e docs(tickets): tuiトークン表示完了 2026-05-04 00:07:59 +09:00
a22cb479f4 docs(tickets): tuiトークン表示レビュー 2026-05-04 00:05:59 +09:00
5efe0e4910 feat: tuiのトークン集計表示の修正 2026-05-04 00:01:37 +09:00
6168e3f924 docs(tickets): TUI表示トークンの集計の修正 2026-05-03 23:28:31 +09:00
9b676238a2 docs(tickets): チケット追加:システムメッセージのTUI表示とセッションのロールバック・フォーク 2026-05-03 22:43:21 +09:00
8df34a1d64 docs(tickets): tui-pod-event-render 完了 (消し忘れ片付け) 2026-05-03 22:14:24 +09:00
45ef661651 update: Taskツール群の説明を更新 2026-05-03 22:09:45 +09:00
2d8767f940 docs(tickets): notify-history-persist 完了 (消し忘れ片付け) 2026-05-03 22:07:18 +09:00
8f7a023897 docs(tickets): session-todo-reminder spec を pending_history_appends に改訂 (AGENTS.md 揮発禁止に整合) 2026-05-03 21:53:20 +09:00
302a1a7f58 Merge branch 'session-todo-tools' into develop
# Conflicts:
#	tickets/session-todo.md
2026-05-03 21:50:30 +09:00
284d07b569 docs(tickets): session-todo (本体) 完了 2026-05-03 21:48:44 +09:00
5fbb9c47dd update: tuiからspawnする際にエラー詳細が落ちていた問題を修正 2026-05-03 21:47:54 +09:00
f18cf7c172 docs(tickets): notify-history-persist完了 2026-05-03 21:37:13 +09:00
cae0c1ea2f docs(tickets): session-todo レビュー反映 (Approve) + reminder spec 段階レビュー 2026-05-03 21:34:54 +09:00
ada1fe6c63 fix: TaskStore snapshot を JSON ブロック化 + 構造ラウンドトリップテスト追加 2026-05-03 21:33:50 +09:00
fde55c96d4 fix: TaskStore snapshot を compact 後 history の末尾に置いて retained 中の TaskCreate 重複を防ぐ 2026-05-03 21:26:49 +09:00
05c2605aae feat: notify-history-persist実装 2026-05-03 19:27:22 +09:00
d1a9b622d4 feat: セッション内 Task ツール (TaskCreate/List/Get/Update + 履歴 replay + compact 跨ぎ) 2026-05-03 19:03:52 +09:00
a87be4cbc2 docs(tickets): セッション内 Task ツールを本体と注意機構に分割 2026-05-03 19:03:48 +09:00
30bb096513 Merge branch 'resume-scope-claim' into develop
# Conflicts:
#	TODO.md
2026-05-03 18:59:01 +09:00
e0261591b6 docs(tickets): resume-scope-claim 完了 2026-05-03 18:56:39 +09:00
eb054b3e88 fix: resume-scope-claim レビュー指摘対応 (deny セマンティクス doc・破損 snapshot の警告ログ) 2026-05-03 18:56:21 +09:00
1be6d34010 docs(tickets): resume-scope-claim レビュー (Approve) 2026-05-03 18:46:15 +09:00
eb0d0433a1 docs(tickets): Notifyが永続化されいない問題についてのチケット 2026-05-03 18:45:10 +09:00
557d5da391 feat: resume時のscope claimを過去の有効scopeに揃える 2026-05-03 17:12:36 +09:00
3f987e9885 feat: session-metrics完了 2026-05-03 15:56:06 +09:00
a86f69fd8d feat: session-metrics実装 2026-05-03 15:10:43 +09:00
cae18a4339 feat: TUIに他Podからの通知を表示する 2026-05-03 12:45:05 +09:00
69a6f63023 docs(tickets): 消し忘れチケットども 2026-05-03 01:16:22 +09:00
1236c68073 chore: TODOから[ ]を削除 2026-05-03 01:08:43 +09:00
d64d1b2ae8 Update AGENTS.md 2026-05-03 01:06:23 +09:00
159ffb0c6d docs(tickets): tuiでPodEventを表示する・セッション中でメトリクスを取るチケットを追加 2026-05-03 01:01:09 +09:00
97a1c10ef7 update: tuiの文字入力のCtrlブロックを追加 2026-05-03 00:44:38 +09:00
eeb570c71f update: memoryシステム周りのプロンプトの整理 2026-05-03 00:27:10 +09:00
9be7caae99 docs(tickets): memory-consolidation-drop-input-cap完了 2026-05-02 23:57:36 +09:00
0e7be01807 update: Consolidationの不要なToken上限の削除 2026-05-02 23:48:33 +09:00
35c8ee3a73 docs(tickets): セッション内TODOツールと注意機構のチケット 2026-05-02 23:48:01 +09:00
c79c54ba9d update: codexのキャッシュ利用が出来てなかった問題 2026-05-02 03:23:44 +09:00
f1d8f42fd5 fix: tuiからのPod作成の挙動を修正・開発時にcargo runでpodを起動する経路を実装 2026-05-02 02:13:30 +09:00
14862fbc37 Merge branch 'workflow-impl' into develop
# Conflicts:
#	crates/pod/src/controller.rs
#	crates/pod/src/pod.rs
2026-05-02 01:47:49 +09:00
ef3f0a8a78 docs(tickets): workflow完了 2026-05-02 01:40:06 +09:00
2ef397b562 update: workflowの実装修正 2026-05-02 01:38:50 +09:00
bebe1169c8 docs(tickets): 消し忘れチケット 2026-05-02 01:36:19 +09:00
ba5b8db9cf feat: dynamic-scopeの実装修正 2026-05-02 01:33:32 +09:00
189ee43a0c feat: dynamic-scopeの実装 2026-05-02 01:26:17 +09:00
6bf1f9a110 fix: SpawnPodの起動経路の問題・を修正 2026-05-02 01:09:57 +09:00
8307ca965c Implement workflow MVP 2026-05-02 00:46:47 +09:00
e97f803104 update: manifestで一部値のzeroの扱いを変更 2026-05-02 00:08:46 +09:00
c4bc994cab fix(llm-worker): openai_responsesのroleの最新の投影を反映 2026-05-01 23:55:26 +09:00
6d84d4df19 chore: dev-depsの整理 2026-05-01 23:50:14 +09:00
ac4133ddf9 docs(tickets): workflowのプロパティ名の修正 2026-05-01 23:40:47 +09:00
6d15d1e2b6 chore: 依存パッケージの集約 2026-05-01 23:35:46 +09:00
ffda357218 Merge branch 'tui-mouse-scroll' into develop 2026-05-01 23:22:58 +09:00
09eb29b0b7 feat: memory P2の修正 2026-05-01 23:22:49 +09:00
300234df57 feat(tui): マウスホイールスクロール完了 2026-05-01 23:16:02 +09:00
7e938b2d3b スキルの整理 2026-05-01 23:14:37 +09:00
0e98d67a5f feat(tui): マウスホイールでスクロールする実装 2026-05-01 23:14:16 +09:00
31eeded4a6 メモリPhase2の実装 2026-05-01 23:00:55 +09:00
ca27d88869 docs: memoryシステムの仕様変更と、動的Tool・VCSの話 2026-05-01 18:47:52 +09:00
38efe82544 bashツール一旦完了 2026-05-01 18:47:09 +09:00
31a1c1d879 bashツール実装 2026-05-01 18:14:13 +09:00
e21f43c70a ClaudeによるTool出力メタ認知 2026-05-01 02:47:44 +09:00
e058dc576d ファイル参照を与えた際に自動的に読ませる実装 2026-04-30 21:58:10 +09:00
a05d7533b0 TUI補完の細かい挙動修正 2026-04-30 14:38:03 +09:00
621acbe224 tuiの補完の実装 2026-04-30 12:46:48 +09:00
e259ab7bd3 claudeの動的ツールの調査レポート 2026-04-30 01:35:42 +09:00
1f3ad13c83 fix: セッション復元時にhistoryが表示されない問題 2026-04-30 00:02:26 +09:00
2c9db5a27b cargo fmt 2026-04-29 23:20:25 +09:00
dcc71e3a14 templatureがcodexエンドポイントで使えない件の修正 2026-04-29 23:20:16 +09:00
426d477584 session-log関連完了 2026-04-29 23:00:55 +09:00
09d56272d8 session-logリファクタのレビュー・修正 2026-04-29 22:55:36 +09:00
de6b8faf55 session-log-segments実装 2026-04-29 22:42:10 +09:00
bb2a6013fa session-log-decouple-item実装 2026-04-29 22:24:18 +09:00
709b17d309 session-storeの永続化形式からllm-workerの内部型を削除 2026-04-29 22:09:30 +09:00
f74716c2e4 tui-input-word-motion完了 2026-04-29 21:45:49 +09:00
f6fe978db4 tui-input-word-motionレビュー・半角カナに関する修正 2026-04-29 21:41:24 +09:00
99d6a4cf4b tuiの単語単位Backspace 2026-04-29 21:31:19 +09:00
9782323885 tuiの単語境界カーソル移動実装 2026-04-29 21:23:29 +09:00
28c2b0eb1c workflowのチケットとtuiの単語境界カーソル移動のチケット 2026-04-29 21:22:49 +09:00
437fe9fe85 pod-registry-rename完了 2026-04-29 21:05:09 +09:00
c647cac983 pod-registry-rename修正 2026-04-29 21:04:47 +09:00
e2d6f00d6d pod-registryのモジュール分割 2026-04-29 20:14:34 +09:00
40d19ca702 scope-lock -> pod-registry 2026-04-29 20:01:32 +09:00
e304b17a7e scope.lockの意味変更に伴うクレート名変更チケット作成 2026-04-29 19:54:08 +09:00
ca0b772242 memory-phase1-extract完了消し忘れ 2026-04-29 19:53:37 +09:00
3962db4d37 tui-session-restore完了 2026-04-29 19:52:24 +09:00
5ea99673fc tuiからセッションを復帰する経路の実装 2026-04-29 19:03:03 +09:00
dad75b592e 不要なforkの削除 2026-04-28 20:19:50 +09:00
d1be97fbc2 resumeの実装 2026-04-28 18:52:58 +09:00
f2b364ec0d max_tokenとreasoning_tokenに関するdocs修正 2026-04-28 18:01:17 +09:00
f1ba5b5686 max_tokensのスキーマ不整合に関する修正 2026-04-28 17:58:24 +09:00
ce7153f6e8 tui-thinking-display完了 2026-04-28 16:23:09 +09:00
04ad20e760 tui-thinking-display修正 2026-04-28 16:22:45 +09:00
fc2c6bc81c TUIにThinkingを表示する実装 2026-04-28 16:10:48 +09:00
31d5de1a37 ThinkingのTUI表示のチケット作成 2026-04-28 16:07:41 +09:00
cfd1879f7e session-store-llm-worker-type-ownership完了 2026-04-28 15:44:16 +09:00
eed3f13e51 セッション関連の責務の分離 2026-04-28 15:43:34 +09:00
a9d30e1c37 memory-phase1の、トークンカウントの実装位置が悪い件 2026-04-28 14:24:38 +09:00
11bd486740 memory-phase1-extract修正 2026-04-28 13:12:21 +09:00
fd88c72e2e memoryを抽出する仕組みの実装 2026-04-28 12:58:33 +09:00
2ef4f26a8f session-restoreの設計更新 2026-04-28 12:42:49 +09:00
cb3642d12c session復帰経路を作るチケット・テスト用のファイルの削除 2026-04-28 12:31:38 +09:00
e4d7cc1924 memoryが.insomnia配下ではなくworkspace root直下を想定していた問題の修正 2026-04-28 11:53:08 +09:00
c4e1a969c1 memoryのクエリと動作のテスト 2026-04-28 11:37:41 +09:00
2e38a24ac2 worker-generation-settings完了 2026-04-28 09:38:23 +09:00
8114d3c4fd 生成設定のmanifest化の実装 2026-04-28 09:37:22 +09:00
cabf9c967c cargo fmt 2026-04-27 22:51:07 +09:00
1c98938b6f model-reasoning-control完了 2026-04-27 22:49:56 +09:00
5fa3d140ab model-reasoning-contolレビュー 2026-04-27 22:41:51 +09:00
7d23cff0a9 model-reasoning-control実装 2026-04-27 22:25:27 +09:00
5246b3ce92 home-dir-layout完了 2026-04-27 22:11:15 +09:00
45ede7a6fc home-dir-layout修正 2026-04-27 22:10:36 +09:00
f8fe6f83aa home-dirの整理 2026-04-27 21:45:30 +09:00
9998539e71 reasoningを利用可能にするチケット 2026-04-27 20:21:22 +09:00
29ea180b18 memory-resident-injection完了 2026-04-27 18:30:21 +09:00
ee60758138 メモリー内容のシステムプロンプトへの埋め込みの実装 2026-04-27 18:25:47 +09:00
db9faa0fad 環境変数に関するチケットの修正 2026-04-27 18:11:40 +09:00
325ae6fa27 pod-spawn-ui完了・設定UI関連のチケット作成 2026-04-27 17:38:32 +09:00
d0a1eaeb57 memory-search-tool完了 2026-04-27 17:26:07 +09:00
56c6758da5 memoryサーチツールを実装 2026-04-27 17:24:08 +09:00
30abefe747 manifest読み込み経路の整理チケット作成 2026-04-27 17:17:00 +09:00
2ed4bd007b manifest側で設定ファイルの収集を行うようにした 2026-04-27 16:52:23 +09:00
5ebdeff76d tuiからSpawnする仮UI 2026-04-27 16:22:06 +09:00
d80d06ff2e memory-file-format完了 2026-04-27 13:59:04 +09:00
f43d8fba3b メモリーに関するクレート作成・ファイル構造の実装 2026-04-27 13:33:31 +09:00
0a676524ae セグメントのセッション永続化チケット 2026-04-27 13:25:16 +09:00
fd89c754f1 submit-segment-protocol完了 2026-04-27 11:42:42 +09:00
2722e0b7ba submitをvec segmentを受け付ける形に変更 2026-04-27 11:03:58 +09:00
e0c4dbdc73 notification-naming完了 2026-04-26 23:30:46 +09:00
e44d49e80f Method::NotifyとEvent::Notificationが紛らわしい問題 2026-04-26 23:25:50 +09:00
123fc3b0ad memory実装チケット 2026-04-26 17:00:38 +09:00
89c2c701fd カタログの実装完了、ドキュメント整理 2026-04-24 13:33:56 +09:00
ce6198102f podのモジュール分割完了 2026-04-24 11:58:11 +09:00
c75d777cec podのモジュール分割 2026-04-24 11:48:27 +09:00
1b1dc73d7f modelsとprovidersをカタログ化 2026-04-24 10:45:03 +09:00
a730717fc7 モデルとプロバイダーをカタログ化するチケット 2026-04-23 16:18:30 +09:00
45b1e7b6de llm-provider-catalog実装 2026-04-23 15:37:51 +09:00
a86c22e6f5 Agents.mdを一定閾値でturncateする仕様を削除 2026-04-23 01:34:25 +09:00
6146b2806f pod-prompt-catalog完了 2026-04-22 17:43:42 +09:00
c68cd64882 Promptを一元管理するファイルから参照する実装 2026-04-22 17:43:05 +09:00
c492765d1a Memoryシステムの整理・Promptカタログチケット 2026-04-22 13:21:15 +09:00
7ce77f0ad5 TUIのEditツール周りの表示とカラー 2026-04-22 01:17:58 +09:00
3717569533 複数クライアント間でのRunメソッドの同期漏れ 2026-04-21 23:59:49 +09:00
676137c246 改行テキストの行計算・Padding設定 2026-04-21 23:26:34 +09:00
84fedd8048 TUIのオーバーホール実装 2026-04-21 23:12:35 +09:00
9bf6378041 protocol-tool-result-shape完了 2026-04-21 20:52:19 +09:00
d4055fb19d TUIに向けたprotocolの詳細調整 2026-04-21 20:50:59 +09:00
3b2bdcb19a TUIオーバーホールチケット 2026-04-21 19:37:14 +09:00
ee694b310f メモリシステムの設計 2026-04-21 19:23:07 +09:00
e513825da9 モデル性能のハードコードを消し飛し、Codexのフォーマットの修正 2026-04-21 18:35:56 +09:00
d37347fe68 Docsのアップロード 2026-04-21 17:39:43 +09:00
30 changed files with 351 additions and 2698 deletions

View File

@ -23,8 +23,7 @@ use crate::spawn::comm_tools::{
use crate::spawn::registry::SpawnedPodRegistry;
use crate::spawn::tool::spawn_pod_tool;
use protocol::{
AlertLevel, AlertSource, ErrorCode, Event, Method, PodStatus, RewindTargetId, RunResult,
Segment, TurnResult,
AlertLevel, AlertSource, ErrorCode, Event, Method, PodStatus, RunResult, Segment, TurnResult,
};
// ---------------------------------------------------------------------------
@ -782,45 +781,6 @@ async fn controller_loop<C, St>(
}
},
Method::ListRewindTargets => match shared_state.get_status() {
PodStatus::Idle | PodStatus::Paused => emit_rewind_targets(&pod, &event_tx),
PodStatus::Running => {
let _ = event_tx.send(Event::Error {
code: ErrorCode::AlreadyRunning,
message: "Pod is already executing a turn; rewind can only run while idle or paused"
.into(),
});
}
},
Method::RewindTo {
target,
expected_head_entries,
} => match shared_state.get_status() {
PodStatus::Idle => {
if apply_rewind(&mut pod, &event_tx, target, expected_head_entries) {
shared_state.set_status(PodStatus::Idle);
let _ = event_tx.send(Event::Status {
status: PodStatus::Idle,
});
}
}
PodStatus::Paused => {
let _ = event_tx.send(Event::Error {
code: ErrorCode::InvalidRequest,
message: "Cannot apply rewind while the Pod is paused; resume or wait for idle first"
.into(),
});
}
PodStatus::Running => {
let _ = event_tx.send(Event::Error {
code: ErrorCode::AlreadyRunning,
message: "Pod is already executing a turn; rewind can only run while idle or paused"
.into(),
});
}
},
Method::Shutdown => {
let _ = event_tx.send(Event::Shutdown);
break;
@ -1054,10 +1014,10 @@ where
message: "Pod is already executing a turn".into(),
});
}
Some(Method::Compact | Method::ListRewindTargets | Method::RewindTo { .. }) => {
Some(Method::Compact) => {
let _ = event_tx.send(Event::Error {
code: ErrorCode::AlreadyRunning,
message: "Pod is already executing a turn; rewind/compact can only run while idle or paused"
message: "Pod is already executing a turn; compact can only run while idle"
.into(),
});
}
@ -1109,70 +1069,6 @@ where
}
}
fn emit_rewind_targets<C, St>(pod: &Pod<C, St>, event_tx: &broadcast::Sender<Event>)
where
C: LlmClient,
St: Store,
{
match pod.list_rewind_targets() {
Ok((head_entries, targets)) => {
let _ = event_tx.send(Event::RewindTargets {
head_entries,
targets,
});
}
Err(err) => {
let _ = event_tx.send(Event::Error {
code: ErrorCode::Internal,
message: err.to_string(),
});
}
}
}
fn apply_rewind<C, St>(
pod: &mut Pod<C, St>,
event_tx: &broadcast::Sender<Event>,
target: RewindTargetId,
expected_head_entries: usize,
) -> bool
where
C: LlmClient,
St: Store,
{
match pod.rewind_to(target, expected_head_entries) {
Ok(applied) => match applied
.entries
.into_iter()
.map(serde_json::to_value)
.collect::<Result<Vec<_>, _>>()
{
Ok(entries) => {
let _ = event_tx.send(Event::RewindApplied {
entries,
input: applied.input,
summary: applied.summary,
});
true
}
Err(error) => {
let _ = event_tx.send(Event::Error {
code: ErrorCode::Internal,
message: format!("failed to encode rewind snapshot: {error}"),
});
false
}
},
Err(err) => {
let _ = event_tx.send(Event::Error {
code: ErrorCode::InvalidRequest,
message: err.to_string(),
});
false
}
}
}
fn build_greeting<C, St>(pod: &Pod<C, St>) -> protocol::Greeting
where
C: LlmClient,

View File

@ -40,9 +40,7 @@ use crate::runtime::pod_registry::{self, ScopeAllocationGuard, ScopeLockError};
use crate::workflow::WorkflowResolveError;
use async_trait::async_trait;
use llm_worker::interceptor::PreRequestAction;
use protocol::{
AlertLevel, AlertSource, Event, RewindSummary, RewindTarget, RewindTargetId, Segment,
};
use protocol::{AlertLevel, AlertSource, Event, Segment};
use tokio::sync::broadcast;
use tokio::task::JoinHandle;
@ -832,85 +830,6 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
&self.store
}
/// List user-submitted turns in newest-first order for the manual rewind picker.
pub fn list_rewind_targets(&self) -> Result<(usize, Vec<RewindTarget>), RewindError> {
let loc = self.segment_state.location();
let entries = self.store.read_all(loc.session_id, loc.segment_id)?;
Ok((
entries.len(),
build_rewind_targets(loc.segment_id, &entries),
))
}
/// Truncate the current segment to just before a previously listed user input.
pub fn rewind_to(
&mut self,
target: RewindTargetId,
expected_head_entries: usize,
) -> Result<RewindAppliedState, RewindError> {
let loc = self.segment_state.location();
if target.segment_id != loc.segment_id {
return Err(RewindError::Invalid(
"rewind target belongs to a different segment".into(),
));
}
let entries = self.store.read_all(loc.session_id, loc.segment_id)?;
if entries.len() != expected_head_entries {
return Err(RewindError::Invalid(format!(
"session head changed since picker opened (expected {expected_head_entries}, current {})",
entries.len()
)));
}
let Some(LogEntry::UserInput { segments, .. }) = entries.get(target.user_input_entry_index)
else {
return Err(RewindError::Invalid(
"rewind target is no longer a user message".into(),
));
};
let input = segments.clone();
let truncate_entries = rewind_truncate_entries(&entries, target.user_input_entry_index);
let retained = entries[..truncate_entries].to_vec();
let tool_side_effect_warning = suffix_has_tool_side_effects(&entries[truncate_entries..]);
let state = segment_log::collect_state(&retained);
let extract_pointer = memory::extract::fold_pointer(&state.extensions);
let task_store = tools::TaskStore::from_history(&state.history);
let summary = RewindSummary {
truncated_to_entries: truncate_entries,
discarded_entries: entries.len().saturating_sub(truncate_entries),
tool_side_effect_warning,
};
self.store
.truncate(loc.session_id, loc.segment_id, truncate_entries)?;
self.segment_state.set_entries_written(truncate_entries);
self.sink.truncate_silent(truncate_entries);
self.worker_mut().set_history(state.history);
self.worker_mut().set_request_config(state.config);
self.worker_mut().set_turn_count(state.turn_count);
self.worker_mut()
.set_last_run_interrupted(state.last_run_interrupted);
self.user_segments = state.user_segments;
*self.usage_history.lock().expect("usage_history poisoned") = state.usage_history;
*self
.pending_attachments
.lock()
.expect("pending_attachments poisoned") = Vec::new();
*self
.extract_pointer
.lock()
.expect("extract_pointer poisoned") = extract_pointer;
self.task_store = task_store;
Ok(RewindAppliedState {
entries: retained,
input,
summary,
})
}
fn write_pod_metadata_pending(&self) -> Result<(), StoreError> {
let Some(writer) = &self.pod_metadata_writer else {
return Ok(());
@ -4409,110 +4328,6 @@ fn token_budget_bytes(tokens: u64) -> usize {
}
/// Pod errors.
#[derive(Debug, thiserror::Error)]
pub enum RewindError {
#[error(transparent)]
Store(#[from] StoreError),
#[error("{0}")]
Invalid(String),
}
#[derive(Debug)]
pub struct RewindAppliedState {
pub entries: Vec<LogEntry>,
pub input: Vec<Segment>,
pub summary: RewindSummary,
}
fn build_rewind_targets(segment_id: uuid::Uuid, entries: &[LogEntry]) -> Vec<RewindTarget> {
let head_entries = entries.len();
let mut turn_index = 0usize;
let mut targets = Vec::new();
for (entry_index, entry) in entries.iter().enumerate() {
if let LogEntry::UserInput { segments, ts } = entry {
turn_index += 1;
let truncate_entries = rewind_truncate_entries(entries, entry_index);
let tool_warning = suffix_has_tool_side_effects(&entries[truncate_entries..]);
targets.push(RewindTarget {
id: RewindTargetId {
segment_id,
user_input_entry_index: entry_index,
},
expected_head_entries: head_entries,
truncate_entries,
turn_index,
timestamp_ms: Some(*ts),
preview: preview_segments(segments),
eligible: true,
disabled_reason: None,
warning: tool_warning.then(|| {
"history suffix will be discarded; tool side effects are not undone".into()
}),
});
}
}
targets.reverse();
targets
}
fn rewind_truncate_entries(entries: &[LogEntry], user_input_entry_index: usize) -> usize {
if user_input_entry_index > 0
&& matches!(
entries.get(user_input_entry_index - 1),
Some(LogEntry::Invoke { .. })
)
{
user_input_entry_index - 1
} else {
user_input_entry_index
}
}
fn suffix_has_tool_side_effects(entries: &[LogEntry]) -> bool {
entries.iter().any(|entry| match entry {
LogEntry::ToolResult { .. } => true,
LogEntry::AssistantItem { item, .. } => logged_item_is_tool_call(item),
_ => false,
})
}
fn logged_item_is_tool_call(item: &session_store::LoggedItem) -> bool {
matches!(item, session_store::LoggedItem::ToolCall { .. })
}
fn preview_segments(segments: &[Segment]) -> String {
let mut preview = String::new();
for segment in segments {
if !preview.is_empty() {
preview.push(' ');
}
match segment {
Segment::Text { content } => preview.push_str(content.trim()),
Segment::Paste { content, .. } => preview.push_str(content.trim()),
Segment::FileRef { path } => {
preview.push('@');
preview.push_str(path);
}
Segment::KnowledgeRef { slug } => {
preview.push('#');
preview.push_str(slug);
}
Segment::WorkflowInvoke { slug } => {
preview.push('/');
preview.push_str(slug);
}
Segment::Unknown => preview.push_str("[unknown input segment]"),
}
}
let preview = preview.replace(['\n', '\r'], " ");
let mut chars = preview.chars();
let mut out: String = chars.by_ref().take(120).collect();
if chars.next().is_some() {
out.push('…');
}
out
}
#[derive(Debug, thiserror::Error)]
pub enum PodError {
#[error(transparent)]
@ -4994,156 +4809,6 @@ mod build_summary_prompt_tests {
}
}
fn text_segment(text: &str) -> Segment {
Segment::Text {
content: text.into(),
}
}
async fn rewind_test_pod() -> (tempfile::TempDir, Pod<NoopClient, session_store::FsStore>) {
let dir = tempfile::tempdir().unwrap();
let manifest = minimal_manifest_with_skills(vec![]);
let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap();
let pwd = dir.path().join("workspace");
std::fs::create_dir_all(&pwd).unwrap();
let scope = Scope::writable(&pwd).unwrap();
let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, pwd, scope)
.await
.unwrap();
pod.ensure_segment_head().unwrap();
(dir, pod)
}
fn append_test_entry(pod: &Pod<NoopClient, session_store::FsStore>, entry: LogEntry) {
let loc = pod.segment_state.location();
pod.store
.append(loc.session_id, loc.segment_id, &entry)
.unwrap();
}
fn append_user_turn(pod: &Pod<NoopClient, session_store::FsStore>, ts: u64, text: &str) {
append_test_entry(
pod,
LogEntry::Invoke {
ts,
trigger: protocol::InvokeKind::UserSend,
},
);
append_test_entry(
pod,
LogEntry::UserInput {
ts: ts + 1,
segments: vec![text_segment(text)],
},
);
append_test_entry(
pod,
LogEntry::TurnEnd {
ts: ts + 2,
turn_count: 1,
},
);
}
#[tokio::test]
async fn rewind_target_listing_is_newest_first_and_warns_on_tool_suffix() {
let (_dir, pod) = rewind_test_pod().await;
append_user_turn(&pod, 10, "first message");
append_user_turn(&pod, 20, "second message");
append_test_entry(
&pod,
LogEntry::ToolResult {
ts: 30,
item: session_store::LoggedItem::ToolResult {
call_id: "call-1".into(),
summary: "wrote a file".into(),
content: None,
is_error: false,
},
},
);
let (head_entries, targets) = pod.list_rewind_targets().unwrap();
let loc = pod.segment_state.location();
assert_eq!(
head_entries,
pod.store
.read_all(loc.session_id, loc.segment_id)
.unwrap()
.len()
);
assert_eq!(targets.len(), 2);
assert_eq!(targets[0].preview, "second message");
assert_eq!(targets[1].preview, "first message");
assert!(
targets[0]
.warning
.as_ref()
.unwrap()
.contains("tool side effects")
);
}
#[tokio::test]
async fn rewind_apply_truncates_log_and_restores_selected_input() {
let (_dir, mut pod) = rewind_test_pod().await;
append_user_turn(&pod, 10, "first message");
append_user_turn(&pod, 20, "second message");
append_test_entry(
&pod,
LogEntry::ToolResult {
ts: 30,
item: session_store::LoggedItem::ToolResult {
call_id: "call-1".into(),
summary: "wrote a file".into(),
content: None,
is_error: false,
},
},
);
let (head_entries, targets) = pod.list_rewind_targets().unwrap();
let expected_truncate_entries = targets[0].truncate_entries;
let target = targets[0].id.clone();
let applied = pod.rewind_to(target, head_entries).unwrap();
assert_eq!(preview_segments(&applied.input), "second message");
assert_eq!(
applied.summary.truncated_to_entries,
expected_truncate_entries
);
assert!(applied.summary.tool_side_effect_warning);
let loc = pod.segment_state.location();
assert_eq!(
pod.store
.read_all(loc.session_id, loc.segment_id)
.unwrap()
.len(),
expected_truncate_entries
);
assert_eq!(pod.worker().history().len(), 1);
assert_eq!(
pod.worker().history()[0].as_text().unwrap(),
"first message"
);
}
#[tokio::test]
async fn rewind_apply_rejects_stale_head() {
let (_dir, mut pod) = rewind_test_pod().await;
append_user_turn(&pod, 10, "first message");
let (head_entries, targets) = pod.list_rewind_targets().unwrap();
append_user_turn(&pod, 20, "newer message");
let err = pod
.rewind_to(targets[0].id.clone(), head_entries)
.unwrap_err()
.to_string();
assert!(err.contains("session head changed"));
}
#[tokio::test]
async fn apply_interrupt_prep_appends_via_callback_and_logs_independent_entries() {
let dir = tempfile::tempdir().unwrap();

View File

@ -36,14 +36,6 @@ pub enum Method {
/// This is a typed control method: clients must not send `compact` as a
/// `Method::Run` user message.
Compact,
/// Ask the Pod to list valid rewind targets from its authoritative session log.
ListRewindTargets,
/// Truncate the current session back to the selected rewind target and
/// return the selected user input to the client composer.
RewindTo {
target: RewindTargetId,
expected_head_entries: usize,
},
Shutdown,
/// Request a list of completion candidates from the Pod.
///
@ -133,7 +125,7 @@ pub enum PodEvent {
/// variants — emits an alert and inserts a `[unknown input segment]`
/// placeholder into the LLM context so neither user nor LLM is blind to
/// the dropped intent.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Segment {
/// Free-form text. The fallback every client can produce.
@ -441,19 +433,6 @@ pub enum Event {
kind: CompletionKind,
entries: Vec<CompletionEntry>,
},
/// Reply to `Method::ListRewindTargets`. Clients should only open a picker
/// in response to their own pending request; the event may be broadcast.
RewindTargets {
head_entries: usize,
targets: Vec<RewindTarget>,
},
/// A rewind has truncated the authoritative session. `entries` is the
/// retained session-log prefix clients should use to reseed display state.
RewindApplied {
entries: Vec<serde_json::Value>,
input: Vec<Segment>,
summary: RewindSummary,
},
/// Reply to `Method::ListVisiblePods`. Payload is a stable JSON value so
/// the Pod crate can evolve discovery fields without introducing a protocol
/// dependency on session-store.
@ -566,34 +545,6 @@ pub struct CompletionEntry {
pub is_dir: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RewindTargetId {
pub segment_id: uuid::Uuid,
pub user_input_entry_index: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RewindTarget {
pub id: RewindTargetId,
pub expected_head_entries: usize,
pub truncate_entries: usize,
pub turn_index: usize,
pub timestamp_ms: Option<u64>,
pub preview: String,
pub eligible: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub disabled_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub warning: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RewindSummary {
pub truncated_to_entries: usize,
pub discarded_entries: usize,
pub tool_side_effect_warning: bool,
}
/// Pod self-description rendered by the TUI when a session starts empty.
///
/// Built once in the Pod controller from the resolved manifest and

View File

@ -2,29 +2,20 @@ use std::collections::VecDeque;
use std::time::Instant;
use protocol::{
AlertLevel, AlertSource, CompletionEntry, CompletionKind, Event, Method, PodStatus,
RewindTarget, RunResult, Segment,
AlertLevel, AlertSource, CompletionEntry, CompletionKind, Event, Method, PodStatus, RunResult,
Segment,
};
use crate::block::{
Block, CompactEvent, ThinkingBlock, ThinkingState, ToolCallBlock, ToolCallState,
};
use crate::cache::FileCache;
use crate::command::{
CommandCandidate, CommandEnvironment, CommandExecution, CommandInputMode, CommandRegistry,
};
use crate::command::{CommandEnvironment, CommandExecution, CommandInputMode, CommandRegistry};
use crate::input::InputBuffer;
use crate::scroll::Scroll;
use crate::task::TaskStore;
use crate::ui::Mode;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandCompletionApply {
Applied,
Ambiguous,
NoCandidates,
}
/// In-flight completion popup state. Lives on `App` while the user is
/// typing inside a `@` / `#` / `/` token. Cleared whenever the trigger
/// is invalidated (cursor moved out, whitespace landed inside the
@ -51,38 +42,6 @@ impl CompletionState {
pub const MAX_VISIBLE: usize = 6;
}
#[derive(Debug, Clone, Default)]
pub struct RewindPickerScroll {
pub top_offset: usize,
pub total_lines: usize,
pub area_height: u16,
pub tail_top_offset: usize,
}
#[derive(Debug, Clone)]
pub struct RewindPickerState {
pub head_entries: usize,
pub targets: Vec<RewindTarget>,
pub selected: usize,
pub scroll: RewindPickerScroll,
}
impl RewindPickerState {
pub fn new(head_entries: usize, targets: Vec<RewindTarget>) -> Self {
let selected = targets.iter().position(|t| t.eligible).unwrap_or(0);
Self {
head_entries,
targets,
selected,
scroll: RewindPickerScroll::default(),
}
}
pub fn selected_target(&self) -> Option<&RewindTarget> {
self.targets.get(self.selected)
}
}
struct RollbackSubmitState {
text: String,
segments: Vec<Segment>,
@ -140,7 +99,6 @@ pub struct App {
pub command_input: InputBuffer,
pub input_mode: CommandInputMode,
pub command_registry: CommandRegistry,
command_completion_selected: Option<usize>,
pub quit: bool,
/// 2-tap guard for `Ctrl-C` when the Pod is not running. First press
/// records the instant; a second press within the timeout exits the
@ -158,10 +116,6 @@ pub struct App {
/// Completion popup state, when an `@` / `#` / `/` token is in
/// flight. `None` whenever the trigger conditions don't hold.
pub completion: Option<CompletionState>,
/// Dedicated main-view rewind picker state.
pub rewind_picker: Option<RewindPickerState>,
rewind_request_pending: bool,
greeting: Option<protocol::Greeting>,
/// In-TUI mirror of the Pod's session task store, reconstructed
/// directly from observed `TaskCreate` / `TaskUpdate` tool calls and
/// `[Session TaskStore snapshot]` system messages — no protocol
@ -204,7 +158,6 @@ impl App {
command_input: InputBuffer::new(),
input_mode: CommandInputMode::Composer,
command_registry: CommandRegistry::default(),
command_completion_selected: None,
quit: false,
quit_confirm: None,
blocks: Vec::new(),
@ -213,9 +166,6 @@ impl App {
cache: FileCache::new(),
assistant_streaming: false,
completion: None,
rewind_picker: None,
rewind_request_pending: false,
greeting: None,
task_store: TaskStore::new(),
task_pane_open: false,
task_pane_scroll: 0,
@ -960,54 +910,6 @@ impl App {
state.selected = 0;
}
}
Event::RewindTargets {
head_entries,
targets,
} => {
if self.rewind_request_pending {
self.rewind_request_pending = false;
self.rewind_picker = Some(RewindPickerState::new(head_entries, targets));
}
}
Event::RewindApplied {
entries,
input,
summary,
} => {
if let Some(greeting) = self.greeting.clone() {
self.restore_snapshot(&entries, greeting);
}
let restored_composer = if self.input.is_empty() {
self.input.replace_with_segments(&input);
true
} else {
false
};
self.completion = None;
self.close_rewind_picker();
self.reset_run_state(self.pod_status);
let mut message = if restored_composer {
format!(
"Rewound session: discarded {} log entries; restored selected input to composer.",
summary.discarded_entries
)
} else {
format!(
"Rewound session: discarded {} log entries. Rewind applied; composer not overwritten because it was not empty.",
summary.discarded_entries
)
};
if summary.tool_side_effect_warning {
message.push_str(
" History suffix was discarded; tool side effects were not undone.",
);
}
self.blocks.push(Block::Alert {
level: AlertLevel::Warn,
source: AlertSource::Pod,
message,
});
}
Event::VisiblePods { .. }
| Event::PodInspection { .. }
| Event::PodAttachRestore { .. } => {}
@ -1176,213 +1078,26 @@ impl App {
pub fn enter_command_mode(&mut self) {
self.input_mode = CommandInputMode::Command;
self.completion = None;
self.command_completion_selected = None;
self.quit_confirm = None;
}
pub fn exit_command_mode(&mut self) {
self.input_mode = CommandInputMode::Composer;
self.command_input.clear();
self.command_completion_selected = None;
}
pub fn clear_command_input(&mut self) {
self.command_input.clear();
self.command_completion_selected = None;
}
pub fn command_text(&self) -> String {
self.command_input.plain_text()
}
pub fn command_suggestions(&self) -> Vec<CommandCandidate> {
pub fn command_suggestions(&self) -> Vec<crate::command::CommandCandidate> {
self.command_registry.suggest(&self.command_text())
}
pub fn command_completion_selected(&self) -> Option<usize> {
let selected = self.command_completion_selected?;
(selected < self.command_suggestions().len()).then_some(selected)
}
pub fn command_completion_active(&self) -> bool {
!self.command_suggestions().is_empty()
}
pub fn move_command_completion_up(&mut self) {
let len = self.command_suggestions().len();
if len == 0 {
self.command_completion_selected = None;
return;
}
self.command_completion_selected = Some(match self.command_completion_selected() {
Some(0) | None => len - 1,
Some(selected) => selected - 1,
});
}
pub fn move_command_completion_down(&mut self) {
let len = self.command_suggestions().len();
if len == 0 {
self.command_completion_selected = None;
return;
}
self.command_completion_selected = Some(match self.command_completion_selected() {
Some(selected) => (selected + 1) % len,
None => 0,
});
}
pub fn apply_command_completion(&mut self) -> CommandCompletionApply {
let suggestions = self.command_suggestions();
let candidate = match self.command_completion_selected() {
Some(selected) => suggestions.get(selected),
None if suggestions.len() == 1 => suggestions.first(),
None if suggestions.is_empty() => return CommandCompletionApply::NoCandidates,
None => return self.ambiguous_command_completion(),
};
let Some(candidate) = candidate else {
self.command_completion_selected = None;
return CommandCompletionApply::NoCandidates;
};
self.replace_command_name(candidate.name);
self.command_completion_selected = None;
CommandCompletionApply::Applied
}
pub fn submit_command_with_completion(&mut self) -> Option<Method> {
let selected = self.command_completion_selected().is_some();
let command_text = self.command_text();
if command_text.trim().is_empty() && !selected {
return self.submit_command();
}
if !selected && self.command_name_is_complete(&command_text) {
return self.submit_command();
}
match self.apply_command_completion() {
CommandCompletionApply::Applied | CommandCompletionApply::NoCandidates => {
self.submit_command()
}
CommandCompletionApply::Ambiguous => None,
}
}
fn ambiguous_command_completion(&mut self) -> CommandCompletionApply {
self.push_command_diagnostic(
"Ambiguous command completion; select a candidate with Up/Down or keep typing.",
);
CommandCompletionApply::Ambiguous
}
fn command_name_is_complete(&self, command_line: &str) -> bool {
let trimmed = command_line.trim_start();
let name = trimmed
.find(char::is_whitespace)
.map(|idx| &trimmed[..idx])
.unwrap_or(trimmed);
!name.is_empty() && self.command_registry.find(name).is_some()
}
fn replace_command_name(&mut self, canonical_name: &str) {
let command_line = self.command_text();
let leading_len = command_line.len() - command_line.trim_start().len();
let after_leading = &command_line[leading_len..];
let name_end = after_leading
.find(char::is_whitespace)
.map(|idx| leading_len + idx)
.unwrap_or(command_line.len());
let rest = &command_line[name_end..];
let mut completed = String::with_capacity(command_line.len().max(canonical_name.len() + 1));
completed.push_str(&command_line[..leading_len]);
completed.push_str(canonical_name);
if rest.is_empty() {
completed.push(' ');
} else {
completed.push_str(rest);
}
self.command_input.clear();
self.command_input.insert_str(&completed);
}
pub fn request_rewind_picker(&mut self) -> Option<Method> {
if !self.connected {
self.push_command_diagnostic("cannot rewind before the Pod is connected");
return None;
}
if self.running {
self.push_command_diagnostic("cannot rewind while the Pod is running");
return None;
}
self.completion = None;
self.rewind_picker = None;
self.rewind_request_pending = true;
Some(Method::ListRewindTargets)
}
pub fn close_rewind_picker(&mut self) {
self.rewind_picker = None;
self.rewind_request_pending = false;
}
pub fn rewind_picker_up(&mut self) {
if let Some(picker) = self.rewind_picker.as_mut() {
if picker.targets.is_empty() {
return;
}
picker.selected = if picker.selected == 0 {
picker.targets.len() - 1
} else {
picker.selected - 1
};
}
}
pub fn rewind_picker_down(&mut self) {
if let Some(picker) = self.rewind_picker.as_mut() {
if !picker.targets.is_empty() {
picker.selected = (picker.selected + 1) % picker.targets.len();
}
}
}
pub fn submit_rewind_picker(&mut self) -> Option<Method> {
if self.paused {
self.push_command_diagnostic(
"cannot apply rewind while the Pod is paused; resume or wait for idle first",
);
return None;
}
if !self.input.is_empty() {
self.push_command_diagnostic(
"cannot apply rewind while composer is not empty; clear it before restoring rewind input",
);
return None;
}
let Some(picker) = self.rewind_picker.as_ref() else {
return None;
};
let Some(target) = picker.selected_target() else {
self.push_command_diagnostic("no rewind target is available");
return None;
};
if !target.eligible {
self.push_command_diagnostic(
target
.disabled_reason
.clone()
.unwrap_or_else(|| "rewind target is disabled".into()),
);
return None;
}
Some(Method::RewindTo {
target: target.id.clone(),
expected_head_entries: target.expected_head_entries,
})
}
fn command_environment(&self) -> CommandEnvironment {
CommandEnvironment {
connected: self.connected,
@ -1404,16 +1119,9 @@ impl App {
}
if result.clear_input {
self.command_input.clear();
self.command_completion_selected = None;
}
if result.exit_command_mode {
self.input_mode = CommandInputMode::Composer;
self.command_completion_selected = None;
}
if let Some(Method::ListRewindTargets) = result.method.as_ref() {
self.completion = None;
self.rewind_picker = None;
self.rewind_request_pending = true;
}
result.method
}
@ -1438,40 +1146,23 @@ impl App {
// stay readable. In command mode these operate on the command line,
// keeping the normal composer buffer intact.
pub fn insert_char(&mut self, c: char) {
let command_mode = self.is_command_mode();
self.active_input_mut().insert_char(c);
if command_mode {
self.command_completion_selected = None;
}
}
pub fn insert_newline(&mut self) {
let command_mode = self.is_command_mode();
self.active_input_mut().insert_newline();
if command_mode {
self.command_completion_selected = None;
}
}
pub fn insert_paste(&mut self, content: String) {
if self.is_command_mode() {
self.command_input.insert_str(&content);
self.command_completion_selected = None;
} else {
self.input.insert_paste(content);
}
}
pub fn delete_char_before(&mut self) {
let command_mode = self.is_command_mode();
self.active_input_mut().delete_before();
if command_mode {
self.command_completion_selected = None;
}
}
pub fn delete_char_after(&mut self) {
let command_mode = self.is_command_mode();
self.active_input_mut().delete_after();
if command_mode {
self.command_completion_selected = None;
}
}
pub fn move_cursor_left(&mut self) {
self.active_input_mut().move_left();
@ -1502,7 +1193,6 @@ impl App {
/// produced. Followed by `Event::Entry` updates for anything
/// committed after the snapshot.
fn restore_snapshot(&mut self, entries: &[serde_json::Value], greeting: protocol::Greeting) {
self.greeting = Some(greeting.clone());
self.context_window = greeting.context_window;
self.session_context_tokens = greeting.context_tokens;
self.turn_index = 0;

View File

@ -147,15 +147,6 @@ impl CommandRegistry {
can_execute: compact_available,
executor: compact_command,
});
registry.register(CommandSpec {
name: "rewind",
aliases: &["rollback"],
usage: "rewind",
description: "Open the rewind target picker.",
argument_parser: rewind_args,
can_execute: rewind_available,
executor: rewind_command,
});
registry
}
@ -293,15 +284,6 @@ fn compact_args(raw: &str) -> Result<CommandArgs, CommandDiagnostic> {
}
}
fn rewind_args(raw: &str) -> Result<CommandArgs, CommandDiagnostic> {
let args = CommandArgs::parse_whitespace(raw);
if args.argv().is_empty() {
Ok(args)
} else {
Err(CommandDiagnostic::new("Invalid arguments. Usage: rewind"))
}
}
fn compact_available(environment: &CommandEnvironment) -> Result<(), CommandDiagnostic> {
if !environment.connected {
return Err(CommandDiagnostic::new(
@ -321,20 +303,6 @@ fn compact_available(environment: &CommandEnvironment) -> Result<(), CommandDiag
Ok(())
}
fn rewind_available(environment: &CommandEnvironment) -> Result<(), CommandDiagnostic> {
if !environment.connected {
return Err(CommandDiagnostic::new(
"Cannot rewind before the Pod is connected.",
));
}
if environment.running {
return Err(CommandDiagnostic::new(
"Cannot rewind while the Pod is running.",
));
}
Ok(())
}
fn help_command(invocation: CommandInvocation<'_>) -> CommandExecution {
if let Some(name) = invocation.args.argv().first() {
let Some(command) = invocation.registry.find(name) else {
@ -382,18 +350,6 @@ fn compact_command(invocation: CommandInvocation<'_>) -> CommandExecution {
}
}
fn rewind_command(invocation: CommandInvocation<'_>) -> CommandExecution {
let _ = invocation.command;
let _ = invocation.environment;
let _ = invocation.args.raw();
CommandExecution {
method: Some(Method::ListRewindTargets),
diagnostics: vec![CommandDiagnostic::new("rewind picker requested")],
exit_command_mode: true,
clear_input: true,
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -465,40 +421,4 @@ mod tests {
assert!(result.method.is_none());
assert!(result.diagnostics[0].message.contains("paused"));
}
#[test]
fn rewind_command_and_alias_return_list_method() {
let registry = CommandRegistry::builtins();
for command in ["rewind", "rollback"] {
let result = registry.dispatch(command, &env());
assert!(matches!(result.method, Some(Method::ListRewindTargets)));
assert!(result.exit_command_mode);
assert!(result.clear_input);
assert!(result.diagnostics[0].message.contains("rewind picker"));
}
}
#[test]
fn rewind_invalid_arguments_are_local_diagnostic() {
let registry = CommandRegistry::builtins();
let result = registry.dispatch("rewind now", &env());
assert!(result.method.is_none());
assert!(!result.exit_command_mode);
assert!(result.diagnostics[0].message.contains("Invalid arguments"));
}
#[test]
fn rewind_rejects_running_but_allows_paused() {
let registry = CommandRegistry::builtins();
let mut running = env();
running.running = true;
let result = registry.dispatch("rewind", &running);
assert!(result.method.is_none());
assert!(result.diagnostics[0].message.contains("running"));
let mut paused = env();
paused.paused = true;
let result = registry.dispatch("rewind", &paused);
assert!(matches!(result.method, Some(Method::ListRewindTargets)));
}
}

View File

@ -144,8 +144,6 @@ pub struct InputBuffer {
atoms: Vec<Atom>,
/// Insertion point in `0..=atoms.len()`.
cursor: usize,
/// Top wrapped row of the visible composer viewport.
scroll_offset: usize,
/// Monotonic counter reused across the TUI process lifetime.
next_paste_id: u32,
}
@ -155,7 +153,6 @@ impl Default for InputBuffer {
Self {
atoms: Vec::new(),
cursor: 0,
scroll_offset: 0,
next_paste_id: 1,
}
}
@ -169,7 +166,6 @@ impl InputBuffer {
pub fn clear(&mut self) {
self.atoms.clear();
self.cursor = 0;
self.scroll_offset = 0;
}
pub fn is_empty(&self) -> bool {
@ -701,27 +697,8 @@ impl InputBuffer {
lines,
cursor_row,
cursor_col,
viewport_start_row: 0,
}
}
/// Clip a full render to `visible_height` rows, updating the stored
/// vertical scroll offset just enough to keep the cursor row visible.
pub fn apply_cursor_viewport(&mut self, render: &mut InputRender, visible_height: u16) {
let height = visible_height.max(1) as usize;
let total_rows = render.lines.len().max(1);
let max_offset = total_rows.saturating_sub(height);
self.scroll_offset = self.scroll_offset.min(max_offset);
let cursor_row = render.cursor_row as usize;
if cursor_row < self.scroll_offset {
self.scroll_offset = cursor_row;
} else if cursor_row >= self.scroll_offset.saturating_add(height) {
self.scroll_offset = cursor_row.saturating_add(1).saturating_sub(height);
}
self.scroll_offset = self.scroll_offset.min(max_offset);
render.apply_viewport(self.scroll_offset, height);
}
}
/// Append a single char, wrapping to a new row first when it would
@ -769,119 +746,6 @@ pub struct InputRender {
pub lines: Vec<Line<'static>>,
pub cursor_row: u16,
pub cursor_col: u16,
/// First wrapped row included in `lines` after viewport clipping.
pub viewport_start_row: u16,
}
impl InputRender {
fn apply_viewport(&mut self, offset: usize, height: usize) {
let offset = offset.min(self.lines.len().saturating_sub(1));
self.viewport_start_row = offset as u16;
self.cursor_row = self.cursor_row.saturating_sub(self.viewport_start_row);
let lines = std::mem::take(&mut self.lines);
self.lines = lines.into_iter().skip(offset).take(height).collect();
if self.lines.is_empty() {
self.lines.push(Line::raw(""));
}
}
}
#[cfg(test)]
mod render_viewport_tests {
use super::*;
fn buf_from(text: &str) -> InputBuffer {
let mut buf = InputBuffer::new();
for c in text.chars() {
buf.insert_char(c);
}
buf
}
fn render_lines(buf: &mut InputBuffer, width: u16, height: u16) -> Vec<String> {
let mut render = buf.render(width);
buf.apply_cursor_viewport(&mut render, height);
render
.lines
.iter()
.map(|line| {
line.spans
.iter()
.map(|span| span.content.as_ref())
.collect()
})
.collect()
}
#[test]
fn short_input_rendering_stays_unscrolled() {
let mut buf = buf_from("one\ntwo");
let mut render = buf.render(20);
buf.apply_cursor_viewport(&mut render, 5);
assert_eq!(buf.scroll_offset, 0);
assert_eq!(render.viewport_start_row, 0);
assert_eq!(render.cursor_row, 1);
assert_eq!(render.cursor_col, 3);
assert_eq!(render_lines(&mut buf, 20, 5), ["one", "two"]);
}
#[test]
fn input_viewport_follows_cursor_at_bottom() {
let mut buf = buf_from("0\n1\n2\n3\n4");
let mut render = buf.render(20);
buf.apply_cursor_viewport(&mut render, 3);
assert_eq!(buf.scroll_offset, 2);
assert_eq!(render.viewport_start_row, 2);
assert_eq!(render.cursor_row, 2);
assert_eq!(render.cursor_col, 1);
assert_eq!(render_lines(&mut buf, 20, 3), ["2", "3", "4"]);
}
#[test]
fn input_viewport_scrolls_when_cursor_moves_above_or_below() {
let mut buf = buf_from("0\n1\n2\n3\n4");
assert_eq!(render_lines(&mut buf, 20, 3), ["2", "3", "4"]);
assert_eq!(buf.scroll_offset, 2);
buf.move_up();
assert_eq!(render_lines(&mut buf, 20, 3), ["2", "3", "4"]);
assert_eq!(buf.scroll_offset, 2);
buf.move_up();
assert_eq!(render_lines(&mut buf, 20, 3), ["2", "3", "4"]);
assert_eq!(buf.scroll_offset, 2);
buf.move_up();
assert_eq!(render_lines(&mut buf, 20, 3), ["1", "2", "3"]);
assert_eq!(buf.scroll_offset, 1);
buf.move_down();
assert_eq!(render_lines(&mut buf, 20, 3), ["1", "2", "3"]);
assert_eq!(buf.scroll_offset, 1);
buf.move_down();
assert_eq!(render_lines(&mut buf, 20, 3), ["1", "2", "3"]);
assert_eq!(buf.scroll_offset, 1);
buf.move_down();
assert_eq!(render_lines(&mut buf, 20, 3), ["2", "3", "4"]);
assert_eq!(buf.scroll_offset, 2);
}
#[test]
fn input_viewport_clamps_after_line_deletion() {
let mut buf = buf_from("0\n1\n2\n3\n4\n5");
assert_eq!(render_lines(&mut buf, 20, 3), ["3", "4", "5"]);
assert_eq!(buf.scroll_offset, 3);
for _ in 0..6 {
buf.delete_before();
}
assert_eq!(render_lines(&mut buf, 20, 3), ["0", "1", "2"]);
assert_eq!(buf.scroll_offset, 0);
}
}
#[cfg(test)]

View File

@ -801,9 +801,6 @@ fn handle_key(app: &mut App, key: KeyEvent) -> Option<Method> {
app.toggle_task_pane();
Some(None)
}
KeyCode::Char(c) if c.eq_ignore_ascii_case(&'r') && ctrl => {
Some(app.request_rewind_picker())
}
KeyCode::Char('a') if ctrl => {
app.move_cursor_start();
Some(app.refresh_completion())
@ -883,25 +880,6 @@ fn handle_key(app: &mut App, key: KeyEvent) -> Option<Method> {
return handle_command_key(app, key);
}
if app.rewind_picker.is_some() {
match key.code {
KeyCode::Esc => {
app.close_rewind_picker();
return None;
}
KeyCode::Enter => return app.submit_rewind_picker(),
KeyCode::Up => {
app.rewind_picker_up();
return None;
}
KeyCode::Down => {
app.rewind_picker_down();
return None;
}
_ => {}
}
}
// Completion popup overrides — only when there's something to
// navigate / commit. An empty popup (request in flight) falls
// through to the default behaviour.
@ -1014,7 +992,7 @@ fn handle_command_key(app: &mut App, key: KeyEvent) -> Option<Method> {
app.exit_command_mode();
None
}
KeyCode::Enter => app.submit_command_with_completion(),
KeyCode::Enter => app.submit_command(),
KeyCode::Backspace => {
if app.command_text().is_empty() {
app.exit_command_mode();
@ -1036,19 +1014,11 @@ fn handle_command_key(app: &mut App, key: KeyEvent) -> Option<Method> {
None
}
KeyCode::Up => {
if app.command_completion_active() {
app.move_command_completion_up();
} else {
app.move_cursor_up();
}
app.move_cursor_up();
None
}
KeyCode::Down => {
if app.command_completion_active() {
app.move_command_completion_down();
} else {
app.move_cursor_down();
}
app.move_cursor_down();
None
}
KeyCode::Home => {
@ -1059,10 +1029,7 @@ fn handle_command_key(app: &mut App, key: KeyEvent) -> Option<Method> {
app.move_cursor_end();
None
}
KeyCode::Tab => {
app.apply_command_completion();
None
}
KeyCode::Tab => None,
KeyCode::Char(c) => {
if key
.modifiers
@ -1101,7 +1068,6 @@ fn handle_pause_or_quit(app: &mut App) -> Option<Method> {
#[cfg(test)]
mod tests {
use super::*;
use protocol::{Event, RewindTarget, RewindTargetId, Segment};
#[test]
fn parse_pod_name_mode() {
@ -1565,191 +1531,6 @@ mod tests {
}));
}
#[test]
fn ctrl_r_requests_rewind_picker_when_idle_or_paused() {
let mut app = App::new("agent".to_string());
app.connected = true;
let idle = handle_key(
&mut app,
KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL),
);
assert!(matches!(idle, Some(Method::ListRewindTargets)));
app.set_pod_status(PodStatus::Paused);
let paused = handle_key(
&mut app,
KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL),
);
assert!(matches!(paused, Some(Method::ListRewindTargets)));
}
#[test]
fn ctrl_r_is_rejected_while_running() {
let mut app = App::new("agent".to_string());
app.connected = true;
app.set_pod_status(PodStatus::Running);
let method = handle_key(
&mut app,
KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL),
);
assert!(method.is_none());
assert!(has_alert(&app, "cannot rewind while the Pod is running"));
}
#[test]
fn rewind_picker_close_returns_to_history_view() {
let mut app = App::new("agent".to_string());
app.connected = true;
app.handle_pod_event(Event::RewindTargets {
head_entries: 1,
targets: vec![],
});
assert!(app.rewind_picker.is_none());
let method = handle_key(
&mut app,
KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL),
);
assert!(matches!(method, Some(Method::ListRewindTargets)));
app.handle_pod_event(Event::RewindTargets {
head_entries: 1,
targets: vec![],
});
assert!(app.rewind_picker.is_some());
let method = handle_key(&mut app, KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
assert!(method.is_none());
assert!(app.rewind_picker.is_none());
}
#[test]
fn rewind_applied_reseeds_display_and_restores_composer() {
let mut app = App::new("agent".to_string());
app.handle_pod_event(Event::Snapshot {
greeting: test_greeting(),
entries: vec![],
status: PodStatus::Idle,
});
app.handle_pod_event(Event::RewindApplied {
entries: vec![],
input: vec![Segment::Text {
content: "retry this".into(),
}],
summary: protocol::RewindSummary {
truncated_to_entries: 0,
discarded_entries: 2,
tool_side_effect_warning: true,
},
});
assert_eq!(input_text(&app), "retry this");
assert!(app.rewind_picker.is_none());
assert!(has_alert(&app, "tool side effects"));
}
#[test]
fn rewind_applied_keeps_non_empty_composer() {
let mut app = App::new("agent".to_string());
app.handle_pod_event(Event::Snapshot {
greeting: test_greeting(),
entries: vec![],
status: PodStatus::Idle,
});
type_keys(&mut app, "draft");
app.handle_pod_event(Event::RewindApplied {
entries: vec![],
input: vec![Segment::Text {
content: "retry this".into(),
}],
summary: protocol::RewindSummary {
truncated_to_entries: 0,
discarded_entries: 2,
tool_side_effect_warning: false,
},
});
assert_eq!(input_text(&app), "draft");
assert!(has_alert(
&app,
"composer not overwritten because it was not empty"
));
}
#[test]
fn rewind_apply_rejects_non_empty_composer_and_paused_status() {
let mut app = App::new("agent".to_string());
app.rewind_picker = Some(crate::app::RewindPickerState::new(1, vec![rewind_target()]));
type_keys(&mut app, "draft");
assert!(app.submit_rewind_picker().is_none());
assert!(has_alert(&app, "composer is not empty"));
let mut app = App::new("agent".to_string());
app.rewind_picker = Some(crate::app::RewindPickerState::new(1, vec![rewind_target()]));
app.set_pod_status(PodStatus::Paused);
assert!(app.submit_rewind_picker().is_none());
assert!(has_alert(
&app,
"cannot apply rewind while the Pod is paused"
));
}
#[test]
fn rewind_picker_draw_does_not_overwrite_history_scroll_state() {
let mut app = App::new("agent".to_string());
app.scroll.top_offset = 3;
app.scroll.turn_starts = vec![0, 5, 9];
app.scroll.total_lines = 42;
app.rewind_picker = Some(crate::app::RewindPickerState::new(1, vec![rewind_target()]));
let original_top_offset = app.scroll.top_offset;
let original_turn_starts = app.scroll.turn_starts.clone();
let original_total_lines = app.scroll.total_lines;
let backend = ratatui::backend::TestBackend::new(80, 24);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
terminal
.draw(|frame| crate::ui::draw(frame, &mut app))
.unwrap();
app.close_rewind_picker();
assert_eq!(app.scroll.top_offset, original_top_offset);
assert_eq!(app.scroll.turn_starts, original_turn_starts);
assert_eq!(app.scroll.total_lines, original_total_lines);
}
fn rewind_target() -> RewindTarget {
RewindTarget {
id: RewindTargetId {
segment_id: uuid::Uuid::nil(),
user_input_entry_index: 0,
},
expected_head_entries: 1,
truncate_entries: 0,
turn_index: 1,
timestamp_ms: Some(1),
preview: "retry this".into(),
eligible: true,
disabled_reason: None,
warning: None,
}
}
fn test_greeting() -> protocol::Greeting {
protocol::Greeting {
pod_name: "agent".into(),
cwd: "/tmp".into(),
provider: "test".into(),
model: "test".into(),
scope_summary: "".into(),
tools: vec![],
context_window: 0,
context_tokens: 0,
}
}
#[test]
fn command_registry_suggestions_are_available() {
let mut app = App::new("agent".to_string());
@ -1777,204 +1558,6 @@ mod tests {
assert_eq!(suggestions[0].name, "noop");
}
#[test]
fn command_completion_tab_applies_unambiguous_candidate() {
let mut app = App::new("agent".to_string());
enter_command_mode(&mut app);
type_keys(&mut app, "no");
assert!(handle_key(&mut app, key(KeyCode::Tab)).is_none());
assert!(app.is_command_mode());
assert_eq!(app.command_text(), "noop ");
assert_eq!(input_text(&app), "");
}
#[test]
fn command_completion_enter_applies_and_executes_unambiguous_candidate() {
let mut app = App::new("agent".to_string());
enter_command_mode(&mut app);
type_keys(&mut app, "no");
let method = handle_key(&mut app, key(KeyCode::Enter));
assert!(method.is_none());
assert!(!app.is_command_mode());
assert_eq!(input_text(&app), "");
assert!(has_alert(&app, "noop: no action"));
}
#[test]
fn command_completion_ambiguous_candidate_requires_selection_or_more_input() {
let mut app = App::new("agent".to_string());
register_test_command(&mut app, "open", "open", parse_no_args, "open executed");
register_test_command(
&mut app,
"options",
"options",
parse_no_args,
"options executed",
);
enter_command_mode(&mut app);
type_keys(&mut app, "o");
assert!(handle_key(&mut app, key(KeyCode::Tab)).is_none());
assert_eq!(app.command_text(), "o");
assert!(app.is_command_mode());
assert!(has_alert(&app, "Ambiguous command completion"));
let before = app.blocks.len();
let method = handle_key(&mut app, key(KeyCode::Enter));
assert!(method.is_none());
assert_eq!(app.command_text(), "o");
assert!(app.is_command_mode());
assert!(app.blocks.len() > before);
assert!(!has_alert(&app, "open executed"));
assert!(!has_alert(&app, "options executed"));
}
#[test]
fn command_completion_selected_candidate_applies_on_enter() {
let mut app = App::new("agent".to_string());
register_test_command(&mut app, "open", "open", parse_no_args, "open executed");
register_test_command(
&mut app,
"options",
"options",
parse_no_args,
"options executed",
);
enter_command_mode(&mut app);
type_keys(&mut app, "o");
assert!(handle_key(&mut app, key(KeyCode::Down)).is_none());
let method = handle_key(&mut app, key(KeyCode::Enter));
assert!(method.is_none());
assert!(!app.is_command_mode());
assert!(has_alert(&app, "open executed"));
assert!(!has_alert(&app, "options executed"));
}
#[test]
fn command_completion_argument_required_keeps_command_mode_after_name_completion() {
let mut app = App::new("agent".to_string());
register_test_command(
&mut app,
"open",
"open <path>",
parse_required_arg,
"open executed",
);
enter_command_mode(&mut app);
type_keys(&mut app, "op");
let method = handle_key(&mut app, key(KeyCode::Enter));
assert!(method.is_none());
assert!(app.is_command_mode());
assert_eq!(app.command_text(), "open ");
assert!(has_alert(&app, "Invalid arguments. Usage: open <path>"));
assert!(!has_alert(&app, "open executed"));
assert_eq!(input_text(&app), "");
}
#[test]
fn command_completion_does_not_affect_normal_composer_without_popup() {
let mut app = App::new("agent".to_string());
type_keys(&mut app, "hello");
assert!(handle_key(&mut app, key(KeyCode::Tab)).is_none());
assert!(!app.is_command_mode());
assert_eq!(input_text(&app), "hello");
}
fn enter_command_mode(app: &mut App) {
assert!(handle_key(app, key(KeyCode::Char(':'))).is_none());
assert!(app.is_command_mode());
}
fn type_keys(app: &mut App, text: &str) {
for c in text.chars() {
assert!(handle_key(app, key(KeyCode::Char(c))).is_none());
}
}
fn key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
fn has_alert(app: &App, needle: &str) -> bool {
app.blocks.iter().any(|block| match block {
crate::block::Block::Alert { message, .. } => message.contains(needle),
_ => false,
})
}
fn register_test_command(
app: &mut App,
name: &'static str,
usage: &'static str,
argument_parser: crate::command::ArgumentParser,
message: &'static str,
) {
app.command_registry.register(crate::command::CommandSpec {
name,
aliases: &[],
usage,
description: "test command",
argument_parser,
can_execute: test_command_available,
executor: test_command_executor,
});
TEST_COMMAND_MESSAGES.with(|messages| messages.borrow_mut().push((name, message)));
}
thread_local! {
static TEST_COMMAND_MESSAGES: std::cell::RefCell<Vec<(&'static str, &'static str)>> =
const { std::cell::RefCell::new(Vec::new()) };
}
fn parse_no_args(
raw: &str,
) -> Result<crate::command::CommandArgs, crate::command::CommandDiagnostic> {
Ok(crate::command::CommandArgs::parse_whitespace(raw))
}
fn parse_required_arg(
raw: &str,
) -> Result<crate::command::CommandArgs, crate::command::CommandDiagnostic> {
let args = crate::command::CommandArgs::parse_whitespace(raw);
if args.argv().is_empty() {
return Err(crate::command::CommandDiagnostic::new(
"Invalid arguments. Usage: open <path>",
));
}
Ok(args)
}
fn test_command_available(
_environment: &crate::command::CommandEnvironment,
) -> Result<(), crate::command::CommandDiagnostic> {
Ok(())
}
fn test_command_executor(
invocation: crate::command::CommandInvocation<'_>,
) -> crate::command::CommandExecution {
let message = TEST_COMMAND_MESSAGES
.with(|messages| {
messages
.borrow()
.iter()
.find(|(name, _)| *name == invocation.command.name)
.map(|(_, message)| *message)
})
.unwrap_or("test command executed");
crate::command::CommandExecution::notice(message)
}
fn input_text(app: &App) -> String {
protocol::Segment::flatten_to_text(&app.input.submit_segments())
}

View File

@ -249,10 +249,6 @@ impl MultiPodApp {
}
}
fn composer_is_blank(&self) -> bool {
segments_are_blank(&self.input.submit_segments())
}
pub(crate) fn prepare_send(&mut self) -> Option<DirectSendRequest> {
let entry = match self.list.selected_entry() {
Some(entry) => entry,
@ -329,7 +325,6 @@ impl MultiPodApp {
self.input.insert_newline();
MultiPodAction::None
}
KeyCode::Enter if self.composer_is_blank() => MultiPodAction::Open,
KeyCode::Enter => self
.prepare_send()
.map(MultiPodAction::Send)
@ -697,10 +692,8 @@ fn multi_pod_layout(area: Rect, input_height: u16) -> MultiPodLayoutState {
fn draw(frame: &mut Frame<'_>, app: &mut MultiPodApp) {
let area = frame.area();
let input_content_width = area.width.saturating_sub(2).max(1);
let mut input_render = app.input.render(input_content_width);
let input_render = app.input.render(input_content_width);
let input_height = input_area_height(&input_render, area.height);
app.input
.apply_cursor_viewport(&mut input_render, input_height);
let layout = multi_pod_layout(area, input_height);
draw_title(frame, layout.title);
@ -880,8 +873,7 @@ fn draw_target_status(frame: &mut Frame<'_>, app: &MultiPodApp, area: Rect) {
fn draw_input(frame: &mut Frame<'_>, render: &crate::input::InputRender, area: Rect) {
let mut lines: Vec<Line<'static>> = Vec::with_capacity(render.lines.len());
for (i, src) in render.lines.iter().enumerate() {
let absolute_row = render.viewport_start_row as usize + i;
let prefix = if absolute_row == 0 { "> " } else { " " };
let prefix = if i == 0 { "> " } else { " " };
let mut spans = vec![Span::styled(prefix, Style::default().fg(Color::DarkGray))];
spans.extend(src.spans.iter().cloned());
lines.push(Line::from(spans));
@ -1207,76 +1199,6 @@ mod tests {
assert!(app.notice.as_deref().unwrap().contains("cannot be opened"));
}
#[test]
fn multi_empty_enter_uses_open_action() {
let mut app = test_app(vec![live_info("alpha", PodStatus::Idle)]);
assert!(matches!(
app.handle_key(key(KeyCode::Enter)),
MultiPodAction::Open
));
let request = app.prepare_open().unwrap();
assert_eq!(request.pod_name, "alpha");
assert_eq!(
request.socket_override,
Some(PathBuf::from("/tmp/alpha.sock"))
);
assert_eq!(input_text(&app), "");
assert!(app.notice.as_deref().unwrap().contains("Opening alpha"));
}
#[test]
fn multi_whitespace_only_enter_uses_open_action() {
let mut app = test_app(vec![live_info("alpha", PodStatus::Idle)]);
app.input.insert_str(" \n\t");
assert!(matches!(
app.handle_key(key(KeyCode::Enter)),
MultiPodAction::Open
));
let request = app.prepare_open().unwrap();
assert_eq!(request.pod_name, "alpha");
assert_eq!(input_text(&app), " \n\t");
}
#[test]
fn multi_non_empty_enter_uses_direct_send_action() {
let mut app = test_app(vec![live_info("idle", PodStatus::Idle)]);
app.input.insert_str("send me");
let request = match app.handle_key(key(KeyCode::Enter)) {
MultiPodAction::Send(request) => request,
_ => panic!("non-empty Enter should direct-send"),
};
assert_eq!(request.socket_path, PathBuf::from("/tmp/idle.sock"));
assert_eq!(Segment::flatten_to_text(&request.segments), "send me");
assert!(app.sending);
assert!(app.notice.as_deref().unwrap().contains("Sending to idle"));
}
#[test]
fn multi_empty_enter_on_non_openable_row_matches_o_diagnostic() {
let mut enter_app = test_app(vec![unreachable_live_info("unreachable")]);
assert!(matches!(
enter_app.handle_key(key(KeyCode::Enter)),
MultiPodAction::Open
));
assert!(enter_app.prepare_open().is_none());
let enter_notice = enter_app.notice.clone();
let mut open_app = test_app(vec![unreachable_live_info("unreachable")]);
assert!(matches!(
open_app.handle_key(key(KeyCode::Char('o'))),
MultiPodAction::Open
));
assert!(open_app.prepare_open().is_none());
assert_eq!(enter_notice, open_app.notice);
}
fn test_app(live: Vec<LivePodInfo>) -> MultiPodApp {
app_with_list(PodList::from_sources(
PodVisibilitySource::ResumePicker,
@ -1316,13 +1238,6 @@ mod tests {
live_info_with_updated_at(pod_name, status, 0)
}
fn unreachable_live_info(pod_name: &str) -> LivePodInfo {
let mut live = live_info(pod_name, PodStatus::Idle);
live.reachable = false;
live.status = None;
live
}
fn live_info_with_updated_at(
pod_name: &str,
status: PodStatus,
@ -1376,8 +1291,4 @@ mod tests {
fn input_text(app: &MultiPodApp) -> String {
Segment::flatten_to_text(&app.input.submit_segments())
}
fn key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
}

View File

@ -65,19 +65,12 @@ pub fn draw(frame: &mut Frame, app: &mut App) {
// Input content starts after the prompt (`> ` or `: `), so the width
// available for wrapping is two columns narrower than the frame.
let input_content_width = area.width.saturating_sub(2).max(1);
let mut input_render = if app.is_command_mode() {
let input_render = if app.is_command_mode() {
app.command_input.render(input_content_width)
} else {
app.input.render(input_content_width)
};
let input_height = input_area_height(&input_render, area.height);
if app.is_command_mode() {
app.command_input
.apply_cursor_viewport(&mut input_render, input_height);
} else {
app.input
.apply_cursor_viewport(&mut input_render, input_height);
}
let mini_view_h = task_mini_view_height(&app.task_store);
// One blank row separates the history tail from the mini-view so
// the latest message doesn't visually crash into the task summary.
@ -291,29 +284,11 @@ fn draw_command_popup(frame: &mut Frame, app: &App, input_area: Rect) {
.add_modifier(Modifier::BOLD);
let description_style = Style::default().fg(Color::DarkGray);
let mut lines: Vec<Line<'static>> = Vec::with_capacity(popup_h as usize);
let selected = app.command_completion_selected();
for (idx, candidate) in visible_suggestions
.iter()
.take(popup_h as usize)
.enumerate()
{
let selected_style = if Some(idx) == selected {
Style::default()
.bg(Color::DarkGray)
.add_modifier(Modifier::BOLD)
} else {
Style::default()
};
for candidate in visible_suggestions.iter().take(popup_h as usize) {
lines.push(Line::from(vec![
Span::styled(
candidate.name.to_owned(),
command_style.patch(selected_style),
),
Span::styled("", description_style.patch(selected_style)),
Span::styled(
candidate.description.to_owned(),
description_style.patch(selected_style),
),
Span::styled(candidate.name.to_owned(), command_style),
Span::styled("", description_style),
Span::styled(candidate.description.to_owned(), description_style),
]));
}
@ -419,11 +394,6 @@ fn draw_history(frame: &mut Frame, app: &mut App, area: Rect) {
return;
}
if let Some(picker) = app.rewind_picker.as_mut() {
draw_rewind_picker(frame, history_area, inner, outer_block, picker);
return;
}
let HistoryLayout { lines, turn_starts } = compute_history(app, inner.width);
// `lines` is already pre-wrapped: 1 entry == 1 terminal row. Scroll
@ -452,99 +422,6 @@ fn draw_history(frame: &mut Frame, app: &mut App, area: Rect) {
.render(history_area, frame.buffer_mut());
}
fn draw_rewind_picker(
frame: &mut Frame,
history_area: Rect,
inner: Rect,
outer_block: UiBlock<'_>,
picker: &mut crate::app::RewindPickerState,
) {
let mut logical: Vec<Line<'static>> = Vec::new();
logical.push(Line::from(vec![
Span::styled(
"Rewind targets",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::raw(format!(" head={} ", picker.head_entries)),
Span::styled("Enter", Style::default().fg(Color::Green)),
Span::raw(" apply "),
Span::styled("Esc", Style::default().fg(Color::Green)),
Span::raw(" cancel"),
]));
logical.push(Line::from(Span::styled(
"Selecting a target discards the later history suffix; tool side effects are not undone.",
Style::default().fg(Color::DarkGray),
)));
logical.push(Line::from(""));
if picker.targets.is_empty() {
logical.push(Line::from(Span::styled(
"No previous user messages are available to rewind.",
Style::default().fg(Color::DarkGray),
)));
} else {
for (idx, target) in picker.targets.iter().enumerate() {
let selected = idx == picker.selected;
let marker = if selected { "" } else { " " };
let base_style = if selected {
Style::default()
.bg(Color::DarkGray)
.add_modifier(Modifier::BOLD)
} else if target.eligible {
Style::default()
} else {
Style::default().fg(Color::DarkGray)
};
let ts = target
.timestamp_ms
.map(|ts| format!("{}", ts))
.unwrap_or_else(|| "-".into());
logical.push(Line::from(vec![
Span::styled(marker.to_owned(), base_style),
Span::styled(
format!(
" turn {} idx {} ts {} ",
target.turn_index, target.id.user_input_entry_index, ts
),
base_style,
),
Span::styled(target.preview.clone(), base_style),
]));
if let Some(warning) = target.warning.as_ref() {
logical.push(Line::from(Span::styled(
format!(" warning: {warning}"),
Style::default().fg(Color::Yellow),
)));
}
if let Some(reason) = target.disabled_reason.as_ref() {
logical.push(Line::from(Span::styled(
format!(" disabled: {reason}"),
Style::default().fg(Color::Red),
)));
}
}
}
let mut lines = Vec::new();
for line in logical {
wrap_line_into(line, inner.width, &mut lines);
}
let tail_top = lines.len().saturating_sub(inner.height as usize);
picker.scroll.area_height = inner.height;
picker.scroll.total_lines = lines.len();
picker.scroll.tail_top_offset = tail_top;
picker.scroll.top_offset = picker.scroll.top_offset.min(tail_top);
let end = (picker.scroll.top_offset + inner.height as usize).min(lines.len());
let visible = lines[picker.scroll.top_offset..end].to_vec();
Paragraph::new(visible)
.block(outer_block)
.render(history_area, frame.buffer_mut());
}
/// Width to reserve for the task side pane within the history rect.
/// Returns 0 when the pane is closed or the rect is too narrow to host
/// it without crushing the history view.
@ -1407,12 +1284,7 @@ fn draw_input(frame: &mut Frame, app: &App, render: &crate::input::InputRender,
};
let mut lines: Vec<Line<'static>> = Vec::with_capacity(render.lines.len());
for (i, src) in render.lines.iter().enumerate() {
let absolute_row = render.viewport_start_row as usize + i;
let prefix = if absolute_row == 0 {
prompt
} else {
continuation
};
let prefix = if i == 0 { prompt } else { continuation };
let mut spans = vec![Span::styled(prefix.to_owned(), prompt_style)];
spans.extend(src.spans.iter().cloned());
lines.push(Line::from(spans));

View File

@ -0,0 +1,217 @@
# Nia 構想
[llm-worker](https://docs.rs/llm-worker/0.2.1/llm_worker/)をベースとして、カスタマイズ可能なToolや、pluginによってワークフローを設計可能にする。
目標は、エージェントを動かすためのエンジンとなり、Dockerのようなエコシステムを構築すること。
- エージェントの活動単位
- プロセス:(MORE SPEC REQUIRED)1プロセス1セッションサブエージェントの扱い方
- ディレクトリ:基本は下位ディレクトリ全体をワーキングディレクトリとして扱う。
サブエージェントに移譲する際は、ディレクトリのexclude又は権限の移譲を必要とし、同時に書き込むことを防ぐ。
- これは単に、ディレクトリツリーの管轄を切り分け、ツールの制限として弾き、また、他のpodが存在するディレクトリは削除・移動出来ないということ
- ネットワーク越し:通常のメッセージパッシングだが、`niad`(デーモンプロセス)を用いる必要がある。
後述の`workspace`単位で公開鍵認証あたりが必要だと考えている
- `workspace`として、複数エージェント間のコミュニケーションや知識を取りまとめる区分を作成する
- 複数のワーカーが存在する以上、可視範囲を作らなければならない
- が、これをローカルマシンに閉じる必要がないという考え
- モジュール・マイクロサービスを総括するためのもの
- (PLAN)スケーラビリティの考え方、大規模にスケールできる必要性(将来的には)
- 永続化
- セッションの履歴を完全に保持する必要はないと考えている。
- ドキュメントとして残し、参照可能にする。
- 直近のセッションは復元可能である必要性がある
- Gitとの相性ドキュメントでのある時点での参照は後々壊れる可能性がある。
- そもそもworkspaceは複数gitレポジトリに跨ることがあるし、workspace自体をgitで管理するべきとするか否か。
- コミットハッシュ+ファイル名みたいな挙動が必要かも。
- [ビルトイン・モジュール](#ビルトインモジュール)で詳細化している。
## エコシステム設計
### 前提
以下の既存の仕組みについて考察する。
- [MCP](https://modelcontextprotocol.io/docs/getting-started/intro)
- [ACP](https://agentclientprotocol.com/overview/introduction)
- [Skills](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview)
- [AGENTS.md](https://agents.md)
### 既存の仕組みとの折り合い
AIのツールを拡張する仕組みに、MCPがある。ツール、データソースの供給、プロンプトのテンプレートを標準化している。
MCPツールをniaのツール定義につなぐ仕組みは既存資源の活用の観点から必須であると考えている。
リソースアクセスは解釈の余地があるが、結局get_resourceみたいなツールとしてAIに渡されるので、Niaが持つResource管理の仕組みに組み込めたら良いかもしれない。
テンプレートはSkillsと合わせて一つの仕組みにするべきだと考えている。
Skillsは任意のタイミングで読める、知らない知識や壊れやすい手順を補助する説明書で、AIにはそれを読むことでタスクの行い方を理解することが期待される。
よって、MCPのPromptsはシステムプロンプトを含めたコンテキストの初期状態から組み立てる必要のあるSkillsと解釈できる。
しかしながら、システムプロンプト含めて構築するのは、サブエージェントでは有効だが、長期間のタスクでは適さないと考えている。
ACPは、エージェントとエディタの通信を定義する仕組み。ACPはセッションの開始要求や操作・能力の通知などを行う仕組みであるから、このシステムには適さない。
また、権限や処理等をエディタに任せる仕組みである為、立場としてはacp対応エージェントをpodでwrapし、niaシステムで利用できるようにするような組み込み方が適していると考えている。
AGENTS.mdは、プロジェクトの全容や知っておくべき内容を記載するドキュメントだが、これはAIに更新させるには原始的すぎで、人が更新するには自由すぎる仕組みだと考えている。
ユーザーの指示はAIが理解して記憶するべきだし、プロジェクトの知識やユーザーの指示を蓄積するよりよい仕組みに代替されるだろうという考え。
### 拡張性に求められる仕組み
llm-workerで目指した、抽象化はするが、より深い制御も可能にする設計は、niaでも必要だと考えている。
MCPからToolsやResourcesを作る仕組みは必要だが、このデフォルト機能をオプトアウトし、ユーザーが自身で仕組みを作れなければ、エコシステムとしては十分でない。
llm-workerとの境界がまさにこれで、エンジンとしてのllm-workerの自由度を利用しつつ、ビルトインの仕組みにプラグインできると良い。
CURDやShellツール、subagentを呼び出すツール等のビルトインツールは提供しつつ、ユーザーが自身で定義するフォーマットも受け入れられる必要がある。
### ワーカーの設計を行う仕組み
なにが出来るかを定義する仕組みがSkillsで、説明と追加のリソースとツールをセットで提供する。
どう動く必要があるかを定義する仕組みがWorkflowsで、出来ること(Skills)に加え、Hooksとロジックを合わせて定義できる。
Workflowはセッション固有で、Skillsは幾つでも定義可能。
Toolsは、KVキャッシュ・コンテキストエンジニアリングの考えに基づき、一度コンテキストに載せたら削除は出来ない。
なぜなら、実行したことのあるツールが存在しない状況はLLMにとって混乱を招く為。
- Tools: ツールを定義する、((比較的)低レイヤな)llm-workerの仕組み
- Hooks: イベントに基づいてWorkerを制御する、(同上)llm-workerの仕組み
- Resources: ReadOnlyな情報源を定義する仕組みで、Toolsを生成する。
- (TODO) 結局はツールなので、そのツールの設計は考える必要がある。
- Skills: LLMに対する動的な指示文を定義する仕組み。ClaudeのSkillsと類似する。
- Workflows:
LLMが行うべき作業の流れを定義する仕組み。Tools/Hooks/Skillsを持てる。
- システムプロンプトを定義し、セッション全体の流れを制約するものであるため、動的にロードできない。ある地点から、今から作業完了モードに入るというよりは、
全体の流れを定義するものにしたい。
- Skillsを内包するが、システマチックに行動を制約する仕組みを提供する。
- `コーディング->lintを通す->レビューをパスする->記録を書く->完了`
みたいな流れを定義可能にしたい。任意の言語でロジックを書けるようにするか、あるいはDSL的に定義するか。
- Profiles:
利用可能なツール、登録されたHooks、最初から与えられるSkills/Workflow、利用可能なSkillsなどをまとめて定義する。
- 利用するプロバイダとモデルや、ビルトインのシステムに対するコンフィグ、サードパーティのプラグインに対するコンフィグなどを纏めて良いかも。
LSP/Toolchainについて言語機能やツールチェーンを呼び出す仕組みは、Hookを用いて編集検知->自動lintフィードバックと、Workflowで編集したらbashでcheckを実行する指示を出せば良い。
また、他のエコシステムとの統合を目指して、LLM/Workerの抽象化層を提供する仕組みを考える必要があるが、
現在のllm-workerでは、llm_clientとして抽象化しているが、拡張機能としてプロバイダをロードする仕組みを提供する必要がある。
### as Plugin/Extension
Workflowを書くことができればそれでハーネスになると考えている。
具体的には、Tool/Hookからデータを収集し、Workflowを進める仕組みなど。
Rhai等のスクリプトを用いて書けると良いと思うが、RustのWasmコンパイルを用いて安全で深い制御ができれば、より良い選択肢になるだろう。
## ビルトイン・モジュール
エコシステムの上に成り立つ、組み込みのモジュールで構成されるシステム。
デフォルトの挙動を形成し、最も重要と言える。
### File Tools
CURDと権限をセットで提供する。AIが何を読み、何を編集したのかを記録できる。
また、先述の管轄ディレクトリ内に権限を制限したり、差分を取得するAPIを提供したりする。
### Companion
ユーザーが主に対話するプロファイルで、直接的にタスクには取り組まず、秘書のように振る舞う。
この役割自体は、Profileとして定義されており、ユーザーの指示を広く受け取るため、Workflowは使われない。
ドキュメントやメモリーを参照したり、ファイルを検索したりして、ユーザーの質問に答えたり、タスクを設計したりする。
常にプロジェクトを把握している役割として振舞うことが期待される。
### Document Resources
ドキュメントを記録する仕組み。
- specドキュメント
- 作業記録
[Readable-Index](#readable-index) を組み込みたい。
### workspace
同じプロジェクトに属するpodをまとめる仕組みで、プロジェクトに応じてpodを分離するための論理的な空間である。
ファイルシステム的な制約は無く、ディレクトリを持つ必要はない。
先述のドキュメントシステムをworkspace単位で構築する仕組みを提供する。
## システムの必要なコンポーネント
- `pod`:実際にファイルを読み書きするプロセス
- `niad`デーモンプロセスniaのコミュニケーションや監視・リストを行う
- `nia-cli`コマンドラインインターフェースniaの操作を行う
- `console`フロントエンドniaの操作を行う
ワークスペースの作成、ダッシュボード、ログ監視、Inboxシステムなど。
当面はWebフロントエンドで作成する。
将来的にはネイティブアプリケーションの方がいいと思う。
---
# 離散的なアイデア
## Podスポーンとサブエージェント
Podが別のPodをスポーンさせるしくみを持つ。複数のエージェントに非同期的にタスクを行わせる為の仕組みである。
同じworkspaceを共有するpodとしてスポーンし、親podは子podのターン完了Hookでループの合間に通知として受け、子podのレスポンスをpollingし、指示を出せる。
この際、編集がコンフリクトしないように、最低限ローカルの書き込み権限を移譲する仕組みが求められる。
これは、1 pod = 1 sessionの仕組みを用いるべきで、
親ディレクトリに居るpodが、子ディレクトリで権限を渡してpodをスポーンさせることで解決できるのではないか。
サブエージェントは、コンテキストを引き継がず、親セッションが必要な情報のみを渡し、より良い指示追従性能を求めるもので、Skillsの、コンテキストを持ったまま新しい技能を仕入れる仕組みよりもより明確に役割を分けることになる。
レビューの結果を再度返して修正し、またサブエージェントに投げるなどで、コンテキストを引き継いでほしいという用途も考えられる。
しかし、互いのアウトプットをプロンプトとする関係上、相互に悪い方向に向かってしまい、元々の要件である指示追従性を損なう可能性がある。
設計思想としては、サブエージェントの単位で相互コミュニケーションはさせるべきではないと考えている。
(TODO)
Skillsとサブエージェントの関係性を考える・サブエージェントの要件を詳細化する
## readable index
ドキュメントや作業記録、プロジェクトの知識などを、**AIが読める形で**インデックス化する仕組み。
すべてのドキュメントのname-descriptionを列挙する仕組みでは、コンテキストを浪費してしまう。
なので、作業記録を書いた際に、サブエージェントをHookから呼び、過去作業との連続性を把握させ、既存のRIをアップデートさせる。
加えて、機械的なlinterを導入してRIの構成や内容をチェックし、品質を保証しないと破綻しそう。
これを作り、AGENTS.mdとして配置する仕組みがあるべきだよなと過去に考えていた。
niaにネイティブに組み込むべきだと思う。
(TODO)ドキュメント化してリンクを張る
## ランタイムAPI
RustそのままでExtensionやPluginを作るのは難しそう。
Rustで書いた拡張をWasmとしてコンパイルしてもらい、それをロードできる仕組みが良い(Zedの拡張機能の仕組みを参考にする)
アーカイブをロード可能にし、スクリプトをロードするので全然良いかも。
## コンテキスト
コンテキスト圧縮のタイミングで、Skillsはアンロード・Workflowは維持し、ここから利用可能なToolsを再構築する仕組みにする。
---
# プロジェクトの存在意義
## 既存のプロジェクトとの比較
ClaudeCode/CodexのようなCLIエージェントと比べて、"エージェントの統括"にフォーカスする。
正直、細部のコーディング能力などはそれらの製品の焼き直しのようになると思うが、オープンソースなエンジンとして、より大きな価値を提供できるものと考えている。
OpenHandsは、ソフトウェアエンジニアリングのタスクを自動化するツールとしての価値を提供する一方、niaは対話的にプロジェクトを統括するツールとしての価値を提供する。

25
docs_old/spec/mvp.md Normal file
View File

@ -0,0 +1,25 @@
# MVP実装目標
## 最小構成
- [ ] Pod: llm-workerをラップし、プロファイル設定で起動
- [ ] niad: Podのライフサイクル管理起動・停止・一覧
- [ ] nia-cli: `nia run <profile>` でPod起動、stdin/stdoutで対話
PodにWorkerを組み込み、daemonからPodを起動する仕組み。を作成する。
ネットワークは考慮せず、一旦は完全にローカルファイルシステムで動作する。
ビルトインツールは最低限作る。(CURD/Shell)
LLMバックエンドを構成可能にする。
## ビルトインツール
- [ ] read_file / write_file / list_dir
- [ ] execute_shell
## Profile形式
- TOML or YAML で定義
- プロバイダ・モデル・システムプロンプト

View File

@ -0,0 +1,7 @@
# nia
`nia` is an agent engine for agentic workflow and automation using LLMs.
# Parts
- **Pod**:

View File

@ -1,155 +0,0 @@
---
id: 20260527-000004-manual-turn-rollback
slug: manual-turn-rollback
title: Pod/TUI: 手動 rewind 導線
status: closed
kind: task
priority: P2
labels: [tui, pod, ux]
created_at: 2026-05-27T00:00:04Z
updated_at: 2026-05-29T03:09:22Z
assignee: null
legacy_ticket: tickets/manual-turn-rollback.md
---
## Background
`pod-empty-turn-rollback` / `tui-empty-turn-restore` により、AI 側出力が 0 の interrupted turn については Pod 側で自動 rollback し、TUI 側で入力を復元できるようになった。
次に欲しいのは、直前 turn だけの rollback command ではなく、TUI から過去の user message を選び、その地点まで会話を戻してその入力を composer に復元する **manual rewind** 導線である。
誤送信、モデル選択ミス、途中で方針を変えた場合などに、ユーザーは過去の入力を選び直し、必要なら編集してから Enter で retry できる。選択した瞬間に再実行はしない。
## UX
- `:rewind` command を追加する。
- `:rollback``:rewind` の alias として扱ってよい。
- `Ctrl+R` は rewind/rollback を表す shortcut として、同じ picker を開く。
- `:rewind` / `Ctrl+R` は引数を取らず、TUI 内の picker を開く。
- Rewind picker は popup/overlay ではなく、通常の conversation/history view area を一時的に置き換える dedicated view として表示する。
- composer/input area と actionbar/status area は通常通り残す。
- main view area だけが message history から rewind target list に切り替わる。
- Esc 等で picker を閉じると、通常の conversation/history view に戻る。
- `:rewind` command は `Idle` / `Paused` の時だけ picker を開く。`Running` 中は visible diagnostic を出して何もしない。
- `Ctrl+R` shortcut も Pod が停止中 (`Idle` または `Paused`) の時だけ有効にする。`Running` 中は無視または visible diagnostic にする。
- picker は過去の user message を新しい順に表示する。
- turn number / index
- timestamp または relative time
- message preview
- eligible / disabled reason
- picker で user message を選択すると、Pod はその user message の直前まで history/session log を rewind し、選択された message を TUI composer に復元する。
- 選択後は、composer に該当 message が入っている状態になる。
- Enter を押すとその message で retry できる。
- ユーザーは送信前に編集できる。
- 選択しただけで自動実行しない。
- Esc 等で picker を閉じると何も変更しない。
## Semantics
Manual rewind は destructive operation として扱う。選択地点より後の履歴 suffix は捨てる。fork は優先度低めの別機能であり、この ticket の実装では fork を作らない。
- Rewind は current active segment/session に対して行う。
- Rewind 成功時、選択された `UserInput` entry 自体も履歴から取り除かれ、composer に戻る。
- Rewind 後、選択地点より後の assistant output / later user messages / usage entries / display blocks は現 branch から消える。
- 元 suffix を保持したい場合は将来の `pod-session-fork` で扱う。この ticket では保持しない。
- Tool side effect の undo はしない。
Initial safety policy:
- Pod が `Idle` または `Paused` の時だけ許可する。
- `Running` 中は拒否する。
- picker 表示時から head が変わった場合は apply 時に再検証して拒否する。
- segment rotation / compaction を跨ぐ rewind は初期実装では対象外でよい。
- suffix に tool call / tool result / other side-effect-looking entries が含まれる場合でも、初期方針としては destructive rewind を許可してよい。ただし UI には「以降の履歴は破棄され、tool side effects は undo されない」ことが分かる notice/diagnostic を出す。
- 実装上どうしても安全に整合性を保てない suffix 種別がある場合は、具体的な disabled reason を表示して拒否する。
## Protocol / ownership
TUI がローカルに履歴を削るのではなく、Pod が authoritative に rewind を検証・適用する。
Suggested protocol shape:
```rust
Method::ListRewindTargets { limit: Option<usize> }
Method::RewindTo {
target: RewindTargetId,
expected_head_entries: usize,
}
Event::RewindTargets { targets: Vec<RewindTarget> }
Event::RewindApplied {
entries: Vec<serde_json::Value>,
input: Vec<Segment>,
summary: RewindSummary,
}
```
Exact names may differ, but the behavior should stay:
- listing targets and applying a target are separate operations.
- apply revalidates target identity and current head.
- success returns enough entries for clients to reseed their view.
- success returns the selected user input segments so TUI can restore the composer.
- failure uses visible diagnostics, e.g. `Event::Error { code: InvalidRequest, message }`.
`RunResult::RolledBack` should not be reused for this idle control operation. It remains the run-lifecycle signal for submit-time empty-turn rollback.
## Implementation notes
- Target identity can initially be current segment + entry index:
```rust
RewindTargetId {
segment_id: SegmentId,
user_input_entry_index: usize,
}
```
- Include `expected_head_entries` to reject stale picker selections.
- Each target should include:
- preview
- original `Vec<Segment>`
- turn/index metadata if available
- whether the target is eligible
- disabled/warning reason if relevant
- the entry count to truncate to, which is before the selected user message.
- Rewind apply must keep these in sync:
- worker history
- `user_segments`
- session store segment log
- `SegmentLogSink` mirror
- usage history / trackers
- TUI view reconstructed from returned entries
- If a complete current-state reconstruction from log is simpler and safer than maintaining many historical snapshots, prefer that over fragile partial truncation.
## Acceptance criteria
- `:rewind` opens a picker of past user messages by replacing the normal conversation/history view area, not by drawing a small popup.
- `Ctrl+R` opens the same picker only while Pod status is `Idle` or `Paused`; it is disabled/rejected while `Running`.
- Selecting a message rewinds the Pod state to before that message and restores the message into the TUI composer.
- Rewind does not auto-run; pressing Enter after selection retries the restored message.
- Rewind success updates Pod session log, SegmentLogSink mirror, worker state, and TUI display consistently.
- Esc returns from the rewind picker to the normal conversation/history view without changing Pod state.
- Rewind failure leaves state unchanged and shows a clear reason.
- Picker selections are revalidated at apply time to avoid stale-head corruption.
- Rewound suffix is intentionally discarded; no fork is created.
- Tool side effects are not undone; UI/diagnostics make this clear when relevant.
- Tests cover target listing, apply success, stale-head rejection, composer restore, TUI display reseed, and at least one suffix-with-tool case.
- `cargo fmt --check`
- `cargo check -p protocol -p pod -p tui`
- Relevant focused tests.
## Out of scope
- Creating a fork when rewinding.
- Fork tree visualization.
- Merging branches.
- Undoing tool side effects.
- Rollback history stack / redo.
- Rewind across compacted segments unless it falls out naturally from implementation.
## Related
- `20260527-000009-pod-session-fork` remains a lower-priority future feature for preserving alternate histories.
- Completed: `pod-empty-turn-rollback`
- Completed: `tui-empty-turn-restore`

View File

@ -1,155 +0,0 @@
---
id: 20260527-000004-manual-turn-rollback
slug: manual-turn-rollback
title: Pod/TUI: 手動 rewind 導線
status: closed
kind: task
priority: P2
labels: [tui, pod, ux]
created_at: 2026-05-27T00:00:04Z
updated_at: 2026-05-29T03:09:22Z
assignee: null
legacy_ticket: tickets/manual-turn-rollback.md
---
## Background
`pod-empty-turn-rollback` / `tui-empty-turn-restore` により、AI 側出力が 0 の interrupted turn については Pod 側で自動 rollback し、TUI 側で入力を復元できるようになった。
次に欲しいのは、直前 turn だけの rollback command ではなく、TUI から過去の user message を選び、その地点まで会話を戻してその入力を composer に復元する **manual rewind** 導線である。
誤送信、モデル選択ミス、途中で方針を変えた場合などに、ユーザーは過去の入力を選び直し、必要なら編集してから Enter で retry できる。選択した瞬間に再実行はしない。
## UX
- `:rewind` command を追加する。
- `:rollback``:rewind` の alias として扱ってよい。
- `Ctrl+R` は rewind/rollback を表す shortcut として、同じ picker を開く。
- `:rewind` / `Ctrl+R` は引数を取らず、TUI 内の picker を開く。
- Rewind picker は popup/overlay ではなく、通常の conversation/history view area を一時的に置き換える dedicated view として表示する。
- composer/input area と actionbar/status area は通常通り残す。
- main view area だけが message history から rewind target list に切り替わる。
- Esc 等で picker を閉じると、通常の conversation/history view に戻る。
- `:rewind` command は `Idle` / `Paused` の時だけ picker を開く。`Running` 中は visible diagnostic を出して何もしない。
- `Ctrl+R` shortcut も Pod が停止中 (`Idle` または `Paused`) の時だけ有効にする。`Running` 中は無視または visible diagnostic にする。
- picker は過去の user message を新しい順に表示する。
- turn number / index
- timestamp または relative time
- message preview
- eligible / disabled reason
- picker で user message を選択すると、Pod はその user message の直前まで history/session log を rewind し、選択された message を TUI composer に復元する。
- 選択後は、composer に該当 message が入っている状態になる。
- Enter を押すとその message で retry できる。
- ユーザーは送信前に編集できる。
- 選択しただけで自動実行しない。
- Esc 等で picker を閉じると何も変更しない。
## Semantics
Manual rewind は destructive operation として扱う。選択地点より後の履歴 suffix は捨てる。fork は優先度低めの別機能であり、この ticket の実装では fork を作らない。
- Rewind は current active segment/session に対して行う。
- Rewind 成功時、選択された `UserInput` entry 自体も履歴から取り除かれ、composer に戻る。
- Rewind 後、選択地点より後の assistant output / later user messages / usage entries / display blocks は現 branch から消える。
- 元 suffix を保持したい場合は将来の `pod-session-fork` で扱う。この ticket では保持しない。
- Tool side effect の undo はしない。
Initial safety policy:
- Pod が `Idle` または `Paused` の時だけ許可する。
- `Running` 中は拒否する。
- picker 表示時から head が変わった場合は apply 時に再検証して拒否する。
- segment rotation / compaction を跨ぐ rewind は初期実装では対象外でよい。
- suffix に tool call / tool result / other side-effect-looking entries が含まれる場合でも、初期方針としては destructive rewind を許可してよい。ただし UI には「以降の履歴は破棄され、tool side effects は undo されない」ことが分かる notice/diagnostic を出す。
- 実装上どうしても安全に整合性を保てない suffix 種別がある場合は、具体的な disabled reason を表示して拒否する。
## Protocol / ownership
TUI がローカルに履歴を削るのではなく、Pod が authoritative に rewind を検証・適用する。
Suggested protocol shape:
```rust
Method::ListRewindTargets { limit: Option<usize> }
Method::RewindTo {
target: RewindTargetId,
expected_head_entries: usize,
}
Event::RewindTargets { targets: Vec<RewindTarget> }
Event::RewindApplied {
entries: Vec<serde_json::Value>,
input: Vec<Segment>,
summary: RewindSummary,
}
```
Exact names may differ, but the behavior should stay:
- listing targets and applying a target are separate operations.
- apply revalidates target identity and current head.
- success returns enough entries for clients to reseed their view.
- success returns the selected user input segments so TUI can restore the composer.
- failure uses visible diagnostics, e.g. `Event::Error { code: InvalidRequest, message }`.
`RunResult::RolledBack` should not be reused for this idle control operation. It remains the run-lifecycle signal for submit-time empty-turn rollback.
## Implementation notes
- Target identity can initially be current segment + entry index:
```rust
RewindTargetId {
segment_id: SegmentId,
user_input_entry_index: usize,
}
```
- Include `expected_head_entries` to reject stale picker selections.
- Each target should include:
- preview
- original `Vec<Segment>`
- turn/index metadata if available
- whether the target is eligible
- disabled/warning reason if relevant
- the entry count to truncate to, which is before the selected user message.
- Rewind apply must keep these in sync:
- worker history
- `user_segments`
- session store segment log
- `SegmentLogSink` mirror
- usage history / trackers
- TUI view reconstructed from returned entries
- If a complete current-state reconstruction from log is simpler and safer than maintaining many historical snapshots, prefer that over fragile partial truncation.
## Acceptance criteria
- `:rewind` opens a picker of past user messages by replacing the normal conversation/history view area, not by drawing a small popup.
- `Ctrl+R` opens the same picker only while Pod status is `Idle` or `Paused`; it is disabled/rejected while `Running`.
- Selecting a message rewinds the Pod state to before that message and restores the message into the TUI composer.
- Rewind does not auto-run; pressing Enter after selection retries the restored message.
- Rewind success updates Pod session log, SegmentLogSink mirror, worker state, and TUI display consistently.
- Esc returns from the rewind picker to the normal conversation/history view without changing Pod state.
- Rewind failure leaves state unchanged and shows a clear reason.
- Picker selections are revalidated at apply time to avoid stale-head corruption.
- Rewound suffix is intentionally discarded; no fork is created.
- Tool side effects are not undone; UI/diagnostics make this clear when relevant.
- Tests cover target listing, apply success, stale-head rejection, composer restore, TUI display reseed, and at least one suffix-with-tool case.
- `cargo fmt --check`
- `cargo check -p protocol -p pod -p tui`
- Relevant focused tests.
## Out of scope
- Creating a fork when rewinding.
- Fork tree visualization.
- Merging branches.
- Undoing tool side effects.
- Rollback history stack / redo.
- Rewind across compacted segments unless it falls out naturally from implementation.
## Related
- `20260527-000009-pod-session-fork` remains a lower-priority future feature for preserving alternate histories.
- Completed: `pod-empty-turn-rollback`
- Completed: `tui-empty-turn-restore`

View File

@ -1,170 +0,0 @@
<!-- event: migration author: tickets.sh-migration at: 2026-05-27T00:00:04Z -->
## Migrated
Migrated from tickets/manual-turn-rollback.md. No legacy review file was present at migration time.
---
<!-- event: close author: hare at: 2026-05-29T03:09:22Z status: closed -->
## Closed
---
id: 20260527-000004-manual-turn-rollback
slug: manual-turn-rollback
title: Pod/TUI: 手動 rewind 導線
status: closed
kind: task
priority: P2
labels: [tui, pod, ux]
created_at: 2026-05-27T00:00:04Z
updated_at: 2026-05-29T03:09:22Z
assignee: null
legacy_ticket: tickets/manual-turn-rollback.md
---
## Background
`pod-empty-turn-rollback` / `tui-empty-turn-restore` により、AI 側出力が 0 の interrupted turn については Pod 側で自動 rollback し、TUI 側で入力を復元できるようになった。
次に欲しいのは、直前 turn だけの rollback command ではなく、TUI から過去の user message を選び、その地点まで会話を戻してその入力を composer に復元する **manual rewind** 導線である。
誤送信、モデル選択ミス、途中で方針を変えた場合などに、ユーザーは過去の入力を選び直し、必要なら編集してから Enter で retry できる。選択した瞬間に再実行はしない。
## UX
- `:rewind` command を追加する。
- `:rollback``:rewind` の alias として扱ってよい。
- `Ctrl+R` は rewind/rollback を表す shortcut として、同じ picker を開く。
- `:rewind` / `Ctrl+R` は引数を取らず、TUI 内の picker を開く。
- Rewind picker は popup/overlay ではなく、通常の conversation/history view area を一時的に置き換える dedicated view として表示する。
- composer/input area と actionbar/status area は通常通り残す。
- main view area だけが message history から rewind target list に切り替わる。
- Esc 等で picker を閉じると、通常の conversation/history view に戻る。
- `:rewind` command は `Idle` / `Paused` の時だけ picker を開く。`Running` 中は visible diagnostic を出して何もしない。
- `Ctrl+R` shortcut も Pod が停止中 (`Idle` または `Paused`) の時だけ有効にする。`Running` 中は無視または visible diagnostic にする。
- picker は過去の user message を新しい順に表示する。
- turn number / index
- timestamp または relative time
- message preview
- eligible / disabled reason
- picker で user message を選択すると、Pod はその user message の直前まで history/session log を rewind し、選択された message を TUI composer に復元する。
- 選択後は、composer に該当 message が入っている状態になる。
- Enter を押すとその message で retry できる。
- ユーザーは送信前に編集できる。
- 選択しただけで自動実行しない。
- Esc 等で picker を閉じると何も変更しない。
## Semantics
Manual rewind は destructive operation として扱う。選択地点より後の履歴 suffix は捨てる。fork は優先度低めの別機能であり、この ticket の実装では fork を作らない。
- Rewind は current active segment/session に対して行う。
- Rewind 成功時、選択された `UserInput` entry 自体も履歴から取り除かれ、composer に戻る。
- Rewind 後、選択地点より後の assistant output / later user messages / usage entries / display blocks は現 branch から消える。
- 元 suffix を保持したい場合は将来の `pod-session-fork` で扱う。この ticket では保持しない。
- Tool side effect の undo はしない。
Initial safety policy:
- Pod が `Idle` または `Paused` の時だけ許可する。
- `Running` 中は拒否する。
- picker 表示時から head が変わった場合は apply 時に再検証して拒否する。
- segment rotation / compaction を跨ぐ rewind は初期実装では対象外でよい。
- suffix に tool call / tool result / other side-effect-looking entries が含まれる場合でも、初期方針としては destructive rewind を許可してよい。ただし UI には「以降の履歴は破棄され、tool side effects は undo されない」ことが分かる notice/diagnostic を出す。
- 実装上どうしても安全に整合性を保てない suffix 種別がある場合は、具体的な disabled reason を表示して拒否する。
## Protocol / ownership
TUI がローカルに履歴を削るのではなく、Pod が authoritative に rewind を検証・適用する。
Suggested protocol shape:
```rust
Method::ListRewindTargets { limit: Option<usize> }
Method::RewindTo {
target: RewindTargetId,
expected_head_entries: usize,
}
Event::RewindTargets { targets: Vec<RewindTarget> }
Event::RewindApplied {
entries: Vec<serde_json::Value>,
input: Vec<Segment>,
summary: RewindSummary,
}
```
Exact names may differ, but the behavior should stay:
- listing targets and applying a target are separate operations.
- apply revalidates target identity and current head.
- success returns enough entries for clients to reseed their view.
- success returns the selected user input segments so TUI can restore the composer.
- failure uses visible diagnostics, e.g. `Event::Error { code: InvalidRequest, message }`.
`RunResult::RolledBack` should not be reused for this idle control operation. It remains the run-lifecycle signal for submit-time empty-turn rollback.
## Implementation notes
- Target identity can initially be current segment + entry index:
```rust
RewindTargetId {
segment_id: SegmentId,
user_input_entry_index: usize,
}
```
- Include `expected_head_entries` to reject stale picker selections.
- Each target should include:
- preview
- original `Vec<Segment>`
- turn/index metadata if available
- whether the target is eligible
- disabled/warning reason if relevant
- the entry count to truncate to, which is before the selected user message.
- Rewind apply must keep these in sync:
- worker history
- `user_segments`
- session store segment log
- `SegmentLogSink` mirror
- usage history / trackers
- TUI view reconstructed from returned entries
- If a complete current-state reconstruction from log is simpler and safer than maintaining many historical snapshots, prefer that over fragile partial truncation.
## Acceptance criteria
- `:rewind` opens a picker of past user messages by replacing the normal conversation/history view area, not by drawing a small popup.
- `Ctrl+R` opens the same picker only while Pod status is `Idle` or `Paused`; it is disabled/rejected while `Running`.
- Selecting a message rewinds the Pod state to before that message and restores the message into the TUI composer.
- Rewind does not auto-run; pressing Enter after selection retries the restored message.
- Rewind success updates Pod session log, SegmentLogSink mirror, worker state, and TUI display consistently.
- Esc returns from the rewind picker to the normal conversation/history view without changing Pod state.
- Rewind failure leaves state unchanged and shows a clear reason.
- Picker selections are revalidated at apply time to avoid stale-head corruption.
- Rewound suffix is intentionally discarded; no fork is created.
- Tool side effects are not undone; UI/diagnostics make this clear when relevant.
- Tests cover target listing, apply success, stale-head rejection, composer restore, TUI display reseed, and at least one suffix-with-tool case.
- `cargo fmt --check`
- `cargo check -p protocol -p pod -p tui`
- Relevant focused tests.
## Out of scope
- Creating a fork when rewinding.
- Fork tree visualization.
- Merging branches.
- Undoing tool side effects.
- Rollback history stack / redo.
- Rewind across compacted segments unless it falls out naturally from implementation.
## Related
- `20260527-000009-pod-session-fork` remains a lower-priority future feature for preserving alternate histories.
- Completed: `pod-empty-turn-rollback`
- Completed: `tui-empty-turn-restore`
---

View File

@ -1,58 +0,0 @@
---
id: 20260529-010200-tui-command-completion-apply
slug: tui-command-completion-apply
title: Apply command completions from keyboard
status: closed
kind: task
priority: P2
labels: [tui, commands, ux]
created_at: 2026-05-29T01:02:00Z
updated_at: 2026-05-29T02:08:56Z
assignee: null
legacy_ticket: null
---
## Background
The TUI command mode (`:`) can show completion candidates, but the candidates cannot currently be applied with keyboard completion keys such as Tab. Also, when there is an unambiguous or selected completion candidate, pressing Enter should be able to complete the command and execute it in one action.
This should make command mode behave like a small command palette rather than a read-only suggestion list.
## Requirements
- Add keyboard application for command completions in command mode.
- Tab should apply the currently selected completion candidate when a candidate exists.
- If there is no explicit selection but exactly one candidate exists, Tab should apply that candidate.
- Applying a command completion should replace the command name prefix with the canonical command name and preserve/position trailing argument editing sensibly.
- Enter behavior should use completion when appropriate.
- If the command input has completion candidates and the current command name is incomplete, Enter should apply the selected/unambiguous candidate and execute the completed command in one action when doing so yields a complete executable command.
- If applying a completion only fills the command name and arguments are still required, Enter should complete the command name and keep command mode active with a helpful state/notice rather than executing an invalid command.
- If no candidate applies, existing command execution/error behavior should remain.
- Completion selection/navigation should be keyboard-accessible.
- Existing up/down behavior should not regress.
- If Tab cycles candidates today for another completion surface, command mode should still have a clear apply path.
- Keep normal composer completion behavior unchanged.
- This ticket is for `:` command mode completion, not file-ref/chip completion in normal input.
- Keep command execution local.
- Commands must not be submitted as user messages.
## Acceptance criteria
- In command mode, typing a command prefix and pressing Tab fills the selected/unambiguous command completion.
- In command mode, typing a command prefix with a selected/unambiguous executable completion and pressing Enter completes and executes it in one action.
- Ambiguous completions do not execute the wrong command silently; they require selection or further typing.
- Commands requiring arguments are not executed with missing arguments just because Enter applied the command name.
- Existing command execution behavior for fully typed commands is unchanged.
- Normal composer/file-ref completion behavior is unchanged.
- Focused tests cover Tab apply, Enter complete-and-execute, ambiguous candidate handling, and argument-required behavior.
- `cargo fmt --check`
- Relevant TUI command tests, e.g. `cargo test -p tui command --no-default-features` or equivalent.
- `cargo check -p tui`
## Out of scope
- New commands.
- Fuzzy matching beyond current prefix/alias suggestions.
- Mouse selection in the completion popup.
- Normal input/file reference completion changes.
- Changing command registry semantics outside completion application.

View File

@ -1,58 +0,0 @@
---
id: 20260529-010200-tui-command-completion-apply
slug: tui-command-completion-apply
title: Apply command completions from keyboard
status: closed
kind: task
priority: P2
labels: [tui, commands, ux]
created_at: 2026-05-29T01:02:00Z
updated_at: 2026-05-29T02:08:56Z
assignee: null
legacy_ticket: null
---
## Background
The TUI command mode (`:`) can show completion candidates, but the candidates cannot currently be applied with keyboard completion keys such as Tab. Also, when there is an unambiguous or selected completion candidate, pressing Enter should be able to complete the command and execute it in one action.
This should make command mode behave like a small command palette rather than a read-only suggestion list.
## Requirements
- Add keyboard application for command completions in command mode.
- Tab should apply the currently selected completion candidate when a candidate exists.
- If there is no explicit selection but exactly one candidate exists, Tab should apply that candidate.
- Applying a command completion should replace the command name prefix with the canonical command name and preserve/position trailing argument editing sensibly.
- Enter behavior should use completion when appropriate.
- If the command input has completion candidates and the current command name is incomplete, Enter should apply the selected/unambiguous candidate and execute the completed command in one action when doing so yields a complete executable command.
- If applying a completion only fills the command name and arguments are still required, Enter should complete the command name and keep command mode active with a helpful state/notice rather than executing an invalid command.
- If no candidate applies, existing command execution/error behavior should remain.
- Completion selection/navigation should be keyboard-accessible.
- Existing up/down behavior should not regress.
- If Tab cycles candidates today for another completion surface, command mode should still have a clear apply path.
- Keep normal composer completion behavior unchanged.
- This ticket is for `:` command mode completion, not file-ref/chip completion in normal input.
- Keep command execution local.
- Commands must not be submitted as user messages.
## Acceptance criteria
- In command mode, typing a command prefix and pressing Tab fills the selected/unambiguous command completion.
- In command mode, typing a command prefix with a selected/unambiguous executable completion and pressing Enter completes and executes it in one action.
- Ambiguous completions do not execute the wrong command silently; they require selection or further typing.
- Commands requiring arguments are not executed with missing arguments just because Enter applied the command name.
- Existing command execution behavior for fully typed commands is unchanged.
- Normal composer/file-ref completion behavior is unchanged.
- Focused tests cover Tab apply, Enter complete-and-execute, ambiguous candidate handling, and argument-required behavior.
- `cargo fmt --check`
- Relevant TUI command tests, e.g. `cargo test -p tui command --no-default-features` or equivalent.
- `cargo check -p tui`
## Out of scope
- New commands.
- Fuzzy matching beyond current prefix/alias suggestions.
- Mouse selection in the completion popup.
- Normal input/file reference completion changes.
- Changing command registry semantics outside completion application.

View File

@ -1,73 +0,0 @@
<!-- event: create author: tickets.sh at: 2026-05-29T01:02:00Z -->
## Created
Created by tickets.sh create.
---
<!-- event: close author: hare at: 2026-05-29T02:08:56Z status: closed -->
## Closed
---
id: 20260529-010200-tui-command-completion-apply
slug: tui-command-completion-apply
title: Apply command completions from keyboard
status: closed
kind: task
priority: P2
labels: [tui, commands, ux]
created_at: 2026-05-29T01:02:00Z
updated_at: 2026-05-29T02:08:56Z
assignee: null
legacy_ticket: null
---
## Background
The TUI command mode (`:`) can show completion candidates, but the candidates cannot currently be applied with keyboard completion keys such as Tab. Also, when there is an unambiguous or selected completion candidate, pressing Enter should be able to complete the command and execute it in one action.
This should make command mode behave like a small command palette rather than a read-only suggestion list.
## Requirements
- Add keyboard application for command completions in command mode.
- Tab should apply the currently selected completion candidate when a candidate exists.
- If there is no explicit selection but exactly one candidate exists, Tab should apply that candidate.
- Applying a command completion should replace the command name prefix with the canonical command name and preserve/position trailing argument editing sensibly.
- Enter behavior should use completion when appropriate.
- If the command input has completion candidates and the current command name is incomplete, Enter should apply the selected/unambiguous candidate and execute the completed command in one action when doing so yields a complete executable command.
- If applying a completion only fills the command name and arguments are still required, Enter should complete the command name and keep command mode active with a helpful state/notice rather than executing an invalid command.
- If no candidate applies, existing command execution/error behavior should remain.
- Completion selection/navigation should be keyboard-accessible.
- Existing up/down behavior should not regress.
- If Tab cycles candidates today for another completion surface, command mode should still have a clear apply path.
- Keep normal composer completion behavior unchanged.
- This ticket is for `:` command mode completion, not file-ref/chip completion in normal input.
- Keep command execution local.
- Commands must not be submitted as user messages.
## Acceptance criteria
- In command mode, typing a command prefix and pressing Tab fills the selected/unambiguous command completion.
- In command mode, typing a command prefix with a selected/unambiguous executable completion and pressing Enter completes and executes it in one action.
- Ambiguous completions do not execute the wrong command silently; they require selection or further typing.
- Commands requiring arguments are not executed with missing arguments just because Enter applied the command name.
- Existing command execution behavior for fully typed commands is unchanged.
- Normal composer/file-ref completion behavior is unchanged.
- Focused tests cover Tab apply, Enter complete-and-execute, ambiguous candidate handling, and argument-required behavior.
- `cargo fmt --check`
- Relevant TUI command tests, e.g. `cargo test -p tui command --no-default-features` or equivalent.
- `cargo check -p tui`
## Out of scope
- New commands.
- Fuzzy matching beyond current prefix/alias suggestions.
- Mouse selection in the completion popup.
- Normal input/file reference completion changes.
- Changing command registry semantics outside completion application.
---

View File

@ -1,55 +0,0 @@
---
id: 20260529-010200-tui-composer-cursor-scroll
slug: tui-composer-cursor-scroll
title: Scroll TUI composer around cursor
status: closed
kind: task
priority: P2
labels: [tui, input, ux]
created_at: 2026-05-29T01:02:00Z
updated_at: 2026-05-29T02:08:04Z
assignee: null
legacy_ticket: null
---
## Background
The TUI composer/input area has a fixed visible height. When the input buffer grows beyond the visible area (for example 10+ lines), the rendered text is clipped instead of scrolling to keep the cursor visible.
This makes editing long messages unreliable: the user can continue typing or moving the cursor, but the relevant lines may be outside the visible area.
## Requirements
- Implement cursor-based vertical scrolling for the normal composer input area.
- The visible viewport should follow the cursor line when the input has more lines than the allocated input height.
- Moving the cursor above the viewport scrolls up.
- Moving the cursor below the viewport scrolls down.
- Typing new lines at the bottom keeps the cursor visible.
- Deleting lines clamps the scroll offset to valid bounds.
- Preserve existing input behavior:
- editing operations.
- cursor movement.
- selection/completion behavior for file refs if applicable.
- queued input behavior.
- command mode behavior unless command input shares the same rendering path and needs the same fix.
- The cursor's terminal position should correspond to the visible cursor location after scrolling.
- The implementation should not simply increase composer height or hide conversation content indefinitely.
- Keep visual separators/borders consistent with the existing TUI layout.
## Acceptance criteria
- A composer buffer longer than the visible input area renders a window around the cursor instead of clipping from a fixed origin.
- Cursor up/down/page movement updates the composer viewport correctly.
- Inserting/deleting lines keeps viewport bounds valid.
- Existing short single-line and small multi-line input rendering remains unchanged.
- Focused tests cover viewport calculation around cursor position and clamping.
- `cargo fmt --check`
- Relevant TUI focused tests, e.g. `cargo test -p tui input --no-default-features` or equivalent.
- `cargo check -p tui`
## Out of scope
- Resizable composer UX redesign.
- Mouse scrolling inside composer.
- Horizontal scrolling/wrapping redesign beyond what is needed to keep current behavior correct.
- Changing command completion behavior; see `20260529-010200-tui-command-completion-apply`.

View File

@ -1,55 +0,0 @@
---
id: 20260529-010200-tui-composer-cursor-scroll
slug: tui-composer-cursor-scroll
title: Scroll TUI composer around cursor
status: closed
kind: task
priority: P2
labels: [tui, input, ux]
created_at: 2026-05-29T01:02:00Z
updated_at: 2026-05-29T02:08:03Z
assignee: null
legacy_ticket: null
---
## Background
The TUI composer/input area has a fixed visible height. When the input buffer grows beyond the visible area (for example 10+ lines), the rendered text is clipped instead of scrolling to keep the cursor visible.
This makes editing long messages unreliable: the user can continue typing or moving the cursor, but the relevant lines may be outside the visible area.
## Requirements
- Implement cursor-based vertical scrolling for the normal composer input area.
- The visible viewport should follow the cursor line when the input has more lines than the allocated input height.
- Moving the cursor above the viewport scrolls up.
- Moving the cursor below the viewport scrolls down.
- Typing new lines at the bottom keeps the cursor visible.
- Deleting lines clamps the scroll offset to valid bounds.
- Preserve existing input behavior:
- editing operations.
- cursor movement.
- selection/completion behavior for file refs if applicable.
- queued input behavior.
- command mode behavior unless command input shares the same rendering path and needs the same fix.
- The cursor's terminal position should correspond to the visible cursor location after scrolling.
- The implementation should not simply increase composer height or hide conversation content indefinitely.
- Keep visual separators/borders consistent with the existing TUI layout.
## Acceptance criteria
- A composer buffer longer than the visible input area renders a window around the cursor instead of clipping from a fixed origin.
- Cursor up/down/page movement updates the composer viewport correctly.
- Inserting/deleting lines keeps viewport bounds valid.
- Existing short single-line and small multi-line input rendering remains unchanged.
- Focused tests cover viewport calculation around cursor position and clamping.
- `cargo fmt --check`
- Relevant TUI focused tests, e.g. `cargo test -p tui input --no-default-features` or equivalent.
- `cargo check -p tui`
## Out of scope
- Resizable composer UX redesign.
- Mouse scrolling inside composer.
- Horizontal scrolling/wrapping redesign beyond what is needed to keep current behavior correct.
- Changing command completion behavior; see `20260529-010200-tui-command-completion-apply`.

View File

@ -1,70 +0,0 @@
<!-- event: create author: tickets.sh at: 2026-05-29T01:02:00Z -->
## Created
Created by tickets.sh create.
---
<!-- event: close author: hare at: 2026-05-29T02:08:04Z status: closed -->
## Closed
---
id: 20260529-010200-tui-composer-cursor-scroll
slug: tui-composer-cursor-scroll
title: Scroll TUI composer around cursor
status: closed
kind: task
priority: P2
labels: [tui, input, ux]
created_at: 2026-05-29T01:02:00Z
updated_at: 2026-05-29T02:08:03Z
assignee: null
legacy_ticket: null
---
## Background
The TUI composer/input area has a fixed visible height. When the input buffer grows beyond the visible area (for example 10+ lines), the rendered text is clipped instead of scrolling to keep the cursor visible.
This makes editing long messages unreliable: the user can continue typing or moving the cursor, but the relevant lines may be outside the visible area.
## Requirements
- Implement cursor-based vertical scrolling for the normal composer input area.
- The visible viewport should follow the cursor line when the input has more lines than the allocated input height.
- Moving the cursor above the viewport scrolls up.
- Moving the cursor below the viewport scrolls down.
- Typing new lines at the bottom keeps the cursor visible.
- Deleting lines clamps the scroll offset to valid bounds.
- Preserve existing input behavior:
- editing operations.
- cursor movement.
- selection/completion behavior for file refs if applicable.
- queued input behavior.
- command mode behavior unless command input shares the same rendering path and needs the same fix.
- The cursor's terminal position should correspond to the visible cursor location after scrolling.
- The implementation should not simply increase composer height or hide conversation content indefinitely.
- Keep visual separators/borders consistent with the existing TUI layout.
## Acceptance criteria
- A composer buffer longer than the visible input area renders a window around the cursor instead of clipping from a fixed origin.
- Cursor up/down/page movement updates the composer viewport correctly.
- Inserting/deleting lines keeps viewport bounds valid.
- Existing short single-line and small multi-line input rendering remains unchanged.
- Focused tests cover viewport calculation around cursor position and clamping.
- `cargo fmt --check`
- Relevant TUI focused tests, e.g. `cargo test -p tui input --no-default-features` or equivalent.
- `cargo check -p tui`
## Out of scope
- Resizable composer UX redesign.
- Mouse scrolling inside composer.
- Horizontal scrolling/wrapping redesign beyond what is needed to keep current behavior correct.
- Changing command completion behavior; see `20260529-010200-tui-command-completion-apply`.
---

View File

@ -1,55 +0,0 @@
---
id: 20260529-031832-multi-pod-empty-enter-open
slug: multi-pod-empty-enter-open
title: Open selected multi-Pod entry on empty Enter
status: closed
kind: task
priority: P2
labels: [tui, pod, ux]
created_at: 2026-05-29T03:18:32Z
updated_at: 2026-05-29T03:27:13Z
assignee: null
legacy_ticket: null
---
## Background
`tui --multi` currently uses `o` to open/attach the selected Pod entry. Enter is used to send the composer contents to the selected idle live Pod.
When the composer is empty, pressing Enter has no message to send. Treat that input as the same action as `o`: open the selected Pod entry in the single-Pod conversation screen.
This should make the multi-Pod dashboard feel more picker-like while preserving direct-send behavior when text is present.
## Requirements
- In `tui --multi`, pressing Enter with an empty composer opens the selected Pod entry, equivalent to pressing `o`.
- Pressing Enter with non-empty composer keeps the current behavior: send the composer contents to the selected eligible idle live Pod.
- Whitespace-only composer should be treated consistently with existing send behavior.
- If current send trims/rejects whitespace-only input as empty, Enter should open.
- If current send treats whitespace as input, preserve that existing behavior.
- Opening must use the existing open path.
- Do not duplicate attach/open logic.
- Existing return-to-multi behavior after detaching from the opened Pod must continue to work.
- Non-openable selected rows should behave like `o` currently behaves.
- Show the same diagnostic/notice and remain in multi view.
- Do not change `o` key behavior.
- Do not change direct-send delivery semantics.
## Acceptance criteria
- `tui --multi`: empty composer + Enter returns the same outcome/action as `o` for an openable selected Pod.
- `tui --multi`: non-empty composer + Enter still direct-sends to the selected eligible idle live Pod.
- Empty Enter on a non-openable row shows the same diagnostic as `o`.
- Existing `o` behavior and return-to-multi behavior remain unchanged.
- Focused tests cover empty Enter open, non-empty Enter send, and non-openable empty Enter diagnostic.
- `cargo fmt --check`
- `cargo test -p tui multi --no-default-features` or equivalent focused tests.
- `cargo check -p tui`
- `git diff --check`
## Out of scope
- Changing direct-send eligibility.
- Adding a new keybinding.
- Changing single-Pod attach behavior.
- Changing multi-Pod row layout.

View File

@ -1,55 +0,0 @@
---
id: 20260529-031832-multi-pod-empty-enter-open
slug: multi-pod-empty-enter-open
title: Open selected multi-Pod entry on empty Enter
status: closed
kind: task
priority: P2
labels: [tui, pod, ux]
created_at: 2026-05-29T03:18:32Z
updated_at: 2026-05-29T03:27:13Z
assignee: null
legacy_ticket: null
---
## Background
`tui --multi` currently uses `o` to open/attach the selected Pod entry. Enter is used to send the composer contents to the selected idle live Pod.
When the composer is empty, pressing Enter has no message to send. Treat that input as the same action as `o`: open the selected Pod entry in the single-Pod conversation screen.
This should make the multi-Pod dashboard feel more picker-like while preserving direct-send behavior when text is present.
## Requirements
- In `tui --multi`, pressing Enter with an empty composer opens the selected Pod entry, equivalent to pressing `o`.
- Pressing Enter with non-empty composer keeps the current behavior: send the composer contents to the selected eligible idle live Pod.
- Whitespace-only composer should be treated consistently with existing send behavior.
- If current send trims/rejects whitespace-only input as empty, Enter should open.
- If current send treats whitespace as input, preserve that existing behavior.
- Opening must use the existing open path.
- Do not duplicate attach/open logic.
- Existing return-to-multi behavior after detaching from the opened Pod must continue to work.
- Non-openable selected rows should behave like `o` currently behaves.
- Show the same diagnostic/notice and remain in multi view.
- Do not change `o` key behavior.
- Do not change direct-send delivery semantics.
## Acceptance criteria
- `tui --multi`: empty composer + Enter returns the same outcome/action as `o` for an openable selected Pod.
- `tui --multi`: non-empty composer + Enter still direct-sends to the selected eligible idle live Pod.
- Empty Enter on a non-openable row shows the same diagnostic as `o`.
- Existing `o` behavior and return-to-multi behavior remain unchanged.
- Focused tests cover empty Enter open, non-empty Enter send, and non-openable empty Enter diagnostic.
- `cargo fmt --check`
- `cargo test -p tui multi --no-default-features` or equivalent focused tests.
- `cargo check -p tui`
- `git diff --check`
## Out of scope
- Changing direct-send eligibility.
- Adding a new keybinding.
- Changing single-Pod attach behavior.
- Changing multi-Pod row layout.

View File

@ -1,70 +0,0 @@
<!-- event: create author: tickets.sh at: 2026-05-29T03:18:32Z -->
## Created
Created by tickets.sh create.
---
<!-- event: close author: hare at: 2026-05-29T03:27:13Z status: closed -->
## Closed
---
id: 20260529-031832-multi-pod-empty-enter-open
slug: multi-pod-empty-enter-open
title: Open selected multi-Pod entry on empty Enter
status: closed
kind: task
priority: P2
labels: [tui, pod, ux]
created_at: 2026-05-29T03:18:32Z
updated_at: 2026-05-29T03:27:13Z
assignee: null
legacy_ticket: null
---
## Background
`tui --multi` currently uses `o` to open/attach the selected Pod entry. Enter is used to send the composer contents to the selected idle live Pod.
When the composer is empty, pressing Enter has no message to send. Treat that input as the same action as `o`: open the selected Pod entry in the single-Pod conversation screen.
This should make the multi-Pod dashboard feel more picker-like while preserving direct-send behavior when text is present.
## Requirements
- In `tui --multi`, pressing Enter with an empty composer opens the selected Pod entry, equivalent to pressing `o`.
- Pressing Enter with non-empty composer keeps the current behavior: send the composer contents to the selected eligible idle live Pod.
- Whitespace-only composer should be treated consistently with existing send behavior.
- If current send trims/rejects whitespace-only input as empty, Enter should open.
- If current send treats whitespace as input, preserve that existing behavior.
- Opening must use the existing open path.
- Do not duplicate attach/open logic.
- Existing return-to-multi behavior after detaching from the opened Pod must continue to work.
- Non-openable selected rows should behave like `o` currently behaves.
- Show the same diagnostic/notice and remain in multi view.
- Do not change `o` key behavior.
- Do not change direct-send delivery semantics.
## Acceptance criteria
- `tui --multi`: empty composer + Enter returns the same outcome/action as `o` for an openable selected Pod.
- `tui --multi`: non-empty composer + Enter still direct-sends to the selected eligible idle live Pod.
- Empty Enter on a non-openable row shows the same diagnostic as `o`.
- Existing `o` behavior and return-to-multi behavior remain unchanged.
- Focused tests cover empty Enter open, non-empty Enter send, and non-openable empty Enter diagnostic.
- `cargo fmt --check`
- `cargo test -p tui multi --no-default-features` or equivalent focused tests.
- `cargo check -p tui`
- `git diff --check`
## Out of scope
- Changing direct-send eligibility.
- Adding a new keybinding.
- Changing single-Pod attach behavior.
- Changing multi-Pod row layout.
---

View File

@ -0,0 +1,74 @@
---
id: 20260527-000004-manual-turn-rollback
slug: manual-turn-rollback
title: Pod/TUI: 手動 rollback 導線
status: open
kind: task
priority: P2
labels: [migrated]
created_at: 2026-05-27T00:00:04Z
updated_at: 2026-05-27T00:00:04Z
assignee: null
legacy_ticket: tickets/manual-turn-rollback.md
---
## Migration reference
- legacy_ticket: tickets/manual-turn-rollback.md
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
# Pod/TUI: 手動 rollback 導線
## 背景
`pod-empty-turn-rollback` / `tui-empty-turn-restore` により、AI 側出力が 0 の interrupted turn については Pod 側で自動 rollback し、TUI 側で入力を復元できるようになった。
一方で、rollback substrate は直前 Run の状態復元に使える形で入り始めているが、ユーザーが明示的に rollback を要求する導線はまだない。誤送信、モデル選択ミス、途中で方針を変えた場合などに、ユーザーが手動で直前状態へ戻す手段が必要になる可能性がある。
詳細な UX / rollback 対象範囲 / safety policy は未決定のため、本チケットでは要求を保持し、実装方針は着手時に確定する。
## 要件メモ
- ユーザーが明示的に rollback を要求できる導線を用意する。
- TUI system command / keybinding / tool / protocol Method のどこに置くかは未決定。
- 最初は TUI から直前 turn を rollback する導線が候補。
- rollback 対象範囲を決める。
- 直前 submit のみか。
- assistant output がある turn を許可するか。
- tool call / tool result が含まれる turn を許可するか。
- 複数 turn rollback は `pod-session-fork` との関係を確認する。
- safety policy を決める。
- user-visible assistant output を消す場合は確認を要求するか。
- tool side effect が既に発生した turn を rollback できるのか、履歴から消すのではなく fork に誘導するのか。
- rollback が history/context 永続化原則を壊さないようにする。
- TUI 側の表示を決める。
- rollback 成功 / 失敗の通知。
- 消された blocks の扱い。
- rollback された input を composer に戻すか、history/backup に置くか。
- protocol signal を整理する。
- 既存 `RunResult::RolledBack` を再利用できるか。
- 手動 rollback は RunEnd ではなく専用 Event / Method が必要か。
## 完了条件(詳細未確定)
- 手動操作で rollback を要求できる。
- rollback 成功時、Pod の session log / SegmentLogSink mirror / TUI 表示が整合する。
- rollback 失敗時、理由がユーザーに見える。
- tool side effect や assistant output を含む turn の扱いが仕様として明示されている。
- tests がある。
- `cargo fmt --check`
- `cargo check --workspace`
- 関連 crate の tests。
## 範囲外
- 複数ターン rollback / 過去地点からの本格的なやり直し(`pod-session-fork` と調整)
- rollback 履歴スタック
- tool side effect の undo
- fork tree 可視化
## 関連
- `tickets/pod-session-fork.md`
- 完了済み: `pod-empty-turn-rollback`
- 完了済み: `tui-empty-turn-restore`

View File

@ -0,0 +1,7 @@
<!-- event: migration author: tickets.sh-migration at: 2026-05-27T00:00:04Z -->
## Migrated
Migrated from tickets/manual-turn-rollback.md. No legacy review file was present at migration time.
---