Compare commits

..

513 Commits

Author SHA1 Message Date
36c24a4c7e ticket: close multi-pod empty enter open 2026-05-29 12:27:14 +09:00
48b569667a merge: multi-pod empty enter open 2026-05-29 12:26:43 +09:00
89393e4623 tui: open multi pod entry on empty enter 2026-05-29 12:23:31 +09:00
3c57b1690b ticket: add multi-pod empty enter open 2026-05-29 12:19:27 +09:00
0f56ca1fc0 ticket: close manual rewind control 2026-05-29 12:09:24 +09:00
4065c0a0f3 merge: manual rewind control 2026-05-29 12:08:27 +09:00
cbb59a47d0 fix: guard manual rewind application 2026-05-29 12:05:33 +09:00
f8881f7289 feat: add manual rewind control 2026-05-29 11:48:56 +09:00
85ffbaf10a ticket: refine manual rewind view 2026-05-29 11:16:13 +09:00
e43e6620b5 ticket: close TUI command completion apply 2026-05-29 11:08:57 +09:00
b904f56b4f merge: TUI command completion apply 2026-05-29 11:08:29 +09:00
3e7b81aa9c ticket: close TUI composer cursor scroll 2026-05-29 11:08:05 +09:00
003d2b584c merge: TUI composer cursor scroll 2026-05-29 11:07:39 +09:00
67c5c4a864 ticket: define manual rewind UX 2026-05-29 10:39:55 +09:00
f6a3d2c6e5 tui: apply command completions from keyboard 2026-05-29 10:15:13 +09:00
208143f01b fix: scroll tui composer around cursor 2026-05-29 10:10:49 +09:00
97f3df651a ticket: add TUI input polish tasks 2026-05-29 10:03:07 +09:00
1da2498295 ticket: close installed binary rename 2026-05-29 09:39:09 +09:00
6bb7882c3e merge: rename installed binaries 2026-05-29 09:39:08 +09:00
06e0bfc359 chore: use static crate fetch for nix vendor 2026-05-29 09:28:49 +09:00
0b23aa8191 fix: rename installed binaries 2026-05-29 09:28:31 +09:00
ee4ccba591 ticket: add installed binary rename 2026-05-29 09:14:07 +09:00
0d7244d0cc ticket: close memory tool guidance prompt 2026-05-29 08:59:07 +09:00
eea40f5095 merge: memory tool guidance prompt 2026-05-29 08:58:47 +09:00
cc5510bc60 ticket: close multi-pod open return 2026-05-29 08:57:50 +09:00
0ecc7f487d merge: multi-pod open return 2026-05-29 08:57:24 +09:00
79da9aa102 prompt: add memory tool usage guidance 2026-05-29 08:49:24 +09:00
be54cb07ea tui: return to multi dashboard after opening pod 2026-05-29 08:45:15 +09:00
eb249dae0c ticket: add multi-pod open return 2026-05-29 08:36:02 +09:00
66408d87e0 ticket: close multi-pod layout polish 2026-05-29 01:49:26 +09:00
be4b175bc5 merge: multi-pod view section layout 2026-05-29 01:49:06 +09:00
d3c54a2407 tui: section multi-pod list layout 2026-05-29 01:46:48 +09:00
258ac2b9a6 ticket: close nix packaging 2026-05-29 01:42:09 +09:00
e58d820c90 merge: nix packaging 2026-05-29 01:41:54 +09:00
401d0912b7 nix: exclude local worktrees from package source 2026-05-29 01:41:09 +09:00
d54d49531f ticket: add multi-pod layout polish 2026-05-29 01:33:35 +09:00
b5e608c597 nix: add installable package 2026-05-29 01:32:04 +09:00
f21642a5a6 ticket: close multi-pod TUI view 2026-05-29 01:09:02 +09:00
b63ff7a11e merge: multi-pod TUI view 2026-05-29 01:08:42 +09:00
6fb7b57054 feat: add multi pod tui dashboard 2026-05-29 01:04:56 +09:00
98c931563e ticket: specify nix package file 2026-05-29 00:56:04 +09:00
7aa48ee4d2 ticket: specify multi-pod TUI entrypoint 2026-05-29 00:53:33 +09:00
f1504c40fd ticket: close TUI pod list abstraction 2026-05-29 00:40:32 +09:00
601ce9f5ac merge: TUI pod list abstraction 2026-05-29 00:39:57 +09:00
7d1db97754 tui: drain initial pod status events 2026-05-29 00:39:00 +09:00
0777bcf299 tui: add pod list model 2026-05-29 00:33:57 +09:00
e4810a7411 ticket: add web tools and nix packaging 2026-05-29 00:31:09 +09:00
142322ef95 ticket: refine TUI pod list abstraction 2026-05-29 00:25:03 +09:00
58981775ec ticket: define multi-pod TUI view 2026-05-28 23:48:39 +09:00
18ba6aee38 ticket: add TUI pod list abstraction 2026-05-28 23:17:16 +09:00
ccb4cd30cd audit: record crate boundary findings 2026-05-28 22:25:54 +09:00
06ebecb329 ticket: close spawnpod initial run confirmation 2026-05-28 22:25:28 +09:00
4998813e8a merge: spawnpod initial run confirmation 2026-05-28 22:24:14 +09:00
d6fd4d1b9c fix: confirm initial SpawnPod run delivery 2026-05-28 22:14:28 +09:00
8703f14a15 ticket: add crate boundary audit 2026-05-28 22:13:45 +09:00
09c2041a88 ticket: refine spawnpod socket delivery 2026-05-28 22:06:47 +09:00
2eab301bf8 ticket: close compact session-log exploration 2026-05-28 18:53:52 +09:00
35d35ef471 test: fix runtime dir expectation 2026-05-28 18:53:52 +09:00
1e938b548c merge: compact session-log exploration 2026-05-28 12:40:37 +09:00
fcca67a9ad style: format manifest paths test 2026-05-28 12:38:53 +09:00
a85ca369ae style: revert unrelated manifest path formatting 2026-05-28 12:36:32 +09:00
11d1dcffb6 merge: main trace diagnostics 2026-05-28 12:32:24 +09:00
12dd35cfb2 feat: add compact session exploration tools 2026-05-28 12:31:44 +09:00
9a92443269 trace: llm stream open diagnostics 2026-05-28 12:26:14 +09:00
c274e4a891 feat: bound compact worker context 2026-05-28 11:59:41 +09:00
7034d02455 ticket: add compact work item metadata 2026-05-28 10:01:28 +09:00
65bbff663f ticket: compact session-log exploration 2026-05-28 10:01:03 +09:00
311d74c25d sanitize: neutralize provider notes and remove claude knowledge 2026-05-28 07:45:49 +09:00
df55af3545 sanitize: remove local path references from current tree 2026-05-28 06:26:34 +09:00
7cb1804504 chore: record spawnpod hang report and local manifest 2026-05-28 06:21:01 +09:00
47cc6234be work-items: close openai unhandled sse observability 2026-05-28 05:44:20 +09:00
dbfdf6aa6c merge: openai unhandled sse observability 2026-05-28 05:44:14 +09:00
60b9cb169a fix: trace unhandled openai responses sse 2026-05-28 05:18:57 +09:00
36f544da18 work-items: add openai unhandled sse observability 2026-05-28 05:13:41 +09:00
1f7bc518cb work-items: add pod orchestration guidance item 2026-05-28 04:45:03 +09:00
56aa241d7b work-items: close tickets sh mvp 2026-05-28 04:29:35 +09:00
1e956c7dff merge: tickets work item thread mvp 2026-05-28 04:27:56 +09:00
3345acafab fix: repair migrated work item encoding 2026-05-28 04:09:47 +09:00
23fc2cf9f4 feat: add tickets work item mvp 2026-05-28 03:59:05 +09:00
1c82058c6f ticket: clarify workitem migration scope 2026-05-28 03:49:21 +09:00
2bb0605650 ticket: complete openai responses diagnostics 2026-05-28 03:23:54 +09:00
a0394a01a6 ticket: record openai responses diagnostics fix 2026-05-28 03:23:25 +09:00
c1a724aedf fix: preserve openai responses incomplete diagnostics 2026-05-28 03:22:53 +09:00
21e48bd2c0 ticket: complete memory consolidation skip observability 2026-05-28 03:09:42 +09:00
a7f1b348de merge: memory consolidation skip observability 2026-05-28 03:09:14 +09:00
11f644fddc ticket: complete llm request timeout fix 2026-05-28 02:44:00 +09:00
23d3b9e070 ticket: record llm request timeout fix 2026-05-28 02:43:23 +09:00
9cd776eaec fix: add llm request lifecycle timeouts 2026-05-28 02:42:31 +09:00
647223eb32 ticket: openai responses incomplete observability 2026-05-28 02:40:30 +09:00
40f4e801dc ticket: llm client request timeouts 2026-05-28 02:07:01 +09:00
dac5fc516f ticket: complete codex oauth wire compatibility 2026-05-28 02:05:49 +09:00
876d75a747 fix: align codex oauth wire behavior 2026-05-28 01:57:04 +09:00
00596d3f9a ticket: codex oauth wire compatibility 2026-05-28 01:44:30 +09:00
8c6a4acf5f fix: suppress memory idle skip notices 2026-05-27 18:55:58 +09:00
ef71bb57d3 ticket: complete compact retained split fix 2026-05-26 21:40:18 +09:00
93373066e0 ticket: record compact retained split fix 2026-05-26 21:39:57 +09:00
8416533695 feat: trace pre-stream lifecycle 2026-05-26 21:05:45 +09:00
372a99bc0b feat: add session stream event trace flag 2026-05-26 19:57:47 +09:00
5ccfdea7c8 fix: compact retained split uses raw tail size 2026-05-26 17:52:09 +09:00
ded02e4c08 ticket: compact retained split usage records 2026-05-26 17:04:29 +09:00
a5b1f15632 ticket: pod scope persistence authority 2026-05-26 16:50:01 +09:00
11e86d3e6e chore: complete memory summary resident injection ticket 2026-05-26 13:29:03 +09:00
670b4b876f chore: complete tui user manifest env overlay ticket 2026-05-26 10:10:00 +09:00
405339fb04 fix: align spawn user manifest env overlay 2026-05-26 10:09:17 +09:00
9435f44d53 merge: memory summary resident injection 2026-05-26 09:55:24 +09:00
fea274cfe3 fix: split resident injection gates 2026-05-26 09:44:24 +09:00
7c42c2b110 docs: add tickets.sh workitem mvp ticket 2026-05-26 09:33:30 +09:00
b1a3b06db7 docs: add memory tool guidance ticket 2026-05-26 09:21:57 +09:00
9ec77a2a2b feat: inject memory summary into resident prompt 2026-05-26 09:21:10 +09:00
962652832d chore: tune project memory thresholds 2026-05-26 09:05:14 +09:00
4a4ff0f6c9 docs: add memory summary resident injection ticket 2026-05-26 08:50:58 +09:00
88619f36cf merge: memory consolidation skip observability 2026-05-26 08:37:32 +09:00
3f260d7d4e fix: confirm SpawnPod initial run delivery 2026-05-26 08:37:24 +09:00
bdffa5120d chore: ignore generated insomnia memory 2026-05-26 08:14:46 +09:00
bb917246ec fix: suppress memory idle skip notices 2026-05-26 08:03:17 +09:00
770173a4ef docs: refine memory consolidation skip ticket 2026-05-26 07:53:37 +09:00
66540172de chore: complete llm retry continuation ticket 2026-05-26 07:22:45 +09:00
be753099ae feat: surface llm retry and continuation state 2026-05-26 07:13:59 +09:00
41402c0951 docs: refine llm retry continuation ticket 2026-05-26 05:20:43 +09:00
fc81555129 docs: note spawnpod delivery race precedent 2026-05-25 07:03:00 +09:00
d9191c393f docs: add spawnpod run delivery ticket 2026-05-25 06:37:38 +09:00
01796f9316 docs: add live pending pod picker ticket 2026-05-25 06:29:13 +09:00
ab73051ddc docs: add memory consolidation skip ticket 2026-05-25 05:43:06 +09:00
9fb11e25a4 docs: specify stream continuation policy 2026-05-25 04:48:07 +09:00
f73055550b chore: complete memory audit log ticket 2026-05-25 03:38:18 +09:00
7003c00d45 merge: memory-audit-log 2026-05-25 03:38:03 +09:00
10d3556792 memory: add audit log events 2026-05-25 03:24:04 +09:00
5b3c579324 docs: add actionbar notice api ticket 2026-05-25 02:40:59 +09:00
08710d808d docs: expand memory audit log ticket 2026-05-25 02:06:42 +09:00
e5fda7efdf fix: refine command mode footer 2026-05-25 01:08:41 +09:00
9224951000 chore: complete tui-system-command-compact ticket 2026-05-24 09:40:41 +09:00
0172414d9a merge: tui-system-command-compact 2026-05-24 09:40:25 +09:00
891b6d91fd test: clean up compact event assertion 2026-05-24 09:39:57 +09:00
cabc556b2c feat: add manual compact command 2026-05-24 08:59:44 +09:00
95d05628e7 chore: complete tui-command-mode ticket 2026-05-24 08:39:25 +09:00
50422ee555 merge: tui-command-mode 2026-05-24 08:38:39 +09:00
14381b8ba5 feat: add TUI command mode 2026-05-24 08:32:21 +09:00
f439de6cdc docs: replace gui mvp with tui spawned pod panel 2026-05-24 08:10:21 +09:00
83cab17f1f docs: split tui command and navigation tickets 2026-05-24 07:59:51 +09:00
b7340eab4b chore: complete worker-history-append-contract ticket 2026-05-24 07:37:29 +09:00
f1c886e451 merge: worker-history-append-contract 2026-05-24 07:37:05 +09:00
a9a2b1e034 docs: split maintainer workflows by role 2026-05-24 07:34:30 +09:00
65c399e6d9 fix: route worker history appends through callbacks 2026-05-24 06:44:19 +09:00
3ae145269c chore: drop stale tui spawn error todo 2026-05-24 06:29:15 +09:00
b64e098b5b chore: complete tui-input-queue ticket 2026-05-23 13:58:09 +09:00
fed3997eb8 merge: tui-input-queue 2026-05-23 13:57:32 +09:00
45bc2265f4 feat: queue tui input during runs 2026-05-23 13:57:22 +09:00
88f755a38f docs: add manual turn rollback ticket 2026-05-23 13:35:03 +09:00
e65b62affa chore: complete tui-empty-turn-restore ticket 2026-05-23 13:30:01 +09:00
2061cea5dd merge: tui-empty-turn-restore 2026-05-23 13:29:07 +09:00
df8f91bda7 feat: restore rolled back tui input 2026-05-23 13:28:56 +09:00
565f152e13 chore: complete pod-empty-turn-rollback ticket 2026-05-23 12:52:42 +09:00
7181910806 merge: pod-empty-turn-rollback 2026-05-23 12:52:12 +09:00
9fc3653502 chore: handle rolled back run result clients 2026-05-23 12:51:40 +09:00
03e7795130 feat: rollback empty interrupted turns 2026-05-23 12:50:46 +09:00
8813d966bb fix: make visible pod list schema object 2026-05-23 12:29:37 +09:00
74ccdd6726 chore: complete pod-discovery-restore-tools ticket 2026-05-23 12:05:30 +09:00
044032ef2b merge: pod-discovery-restore-tools 2026-05-23 12:04:59 +09:00
fd1b06198e feat: add visible pod discovery tools 2026-05-23 12:04:45 +09:00
aea33efaeb chore: complete memory-extract-remove-input-cap ticket 2026-05-23 09:14:37 +09:00
cf4ecf8d70 merge: memory-extract-remove-input-cap 2026-05-23 09:14:15 +09:00
d5da95499d fix: remove memory extract input cap 2026-05-23 09:14:07 +09:00
ba92581d51 chore: complete tui-pod-restore-picker ticket 2026-05-23 09:13:57 +09:00
c4183d5ba6 merge: tui-pod-restore-picker 2026-05-23 09:13:19 +09:00
a8b311bd1f feat: restore tui sessions by pod 2026-05-23 09:13:06 +09:00
cfb5fa89f1 chore: complete spawned-delegation-scope-reclaim ticket 2026-05-23 08:39:04 +09:00
c2bcaac03d merge: spawned-delegation-scope-reclaim 2026-05-23 08:38:50 +09:00
a26f18c466 fix: reclaim delegated scope from stopped children 2026-05-23 08:38:42 +09:00
0cba8e9f5c docs: refine pod visibility and tui restore flow 2026-05-23 08:33:00 +09:00
ccc6efc0e6 update: tui -rの際のリストの時系列ソート 2026-05-23 08:02:05 +09:00
942ab0e15b chore: complete tui-streaming-input-loss ticket 2026-05-23 07:16:08 +09:00
0ff39f33bb merge: tui-streaming-input-loss 2026-05-23 07:15:55 +09:00
7480a5732f fix: preserve tui input during streaming 2026-05-23 07:15:39 +09:00
c1d4d0b65c chore: complete tui-context-usage-indicator ticket 2026-05-23 07:15:30 +09:00
aa57253e39 merge: tui-context-usage-indicator 2026-05-23 07:15:17 +09:00
a267ad8114 feat: show context usage in tui status 2026-05-23 07:15:03 +09:00
637694893a docs: identify tui streaming input loss race 2026-05-23 05:47:59 +09:00
be8b10e759 Create tui-parts.md 2026-05-23 05:41:48 +09:00
60daf64808 fix: tighten task tool usage guidance 2026-05-23 05:11:48 +09:00
01ccac7cf1 chore: complete prune-token-budget ticket 2026-05-23 05:00:30 +09:00
369a5931bc merge: prune-token-budget 2026-05-23 05:00:15 +09:00
9ee7f04805 feat: protect prune tail by token budget 2026-05-23 05:00:06 +09:00
0d30b6139c chore: complete pod-event-callback-delivery ticket 2026-05-23 04:57:26 +09:00
f4627de3ee merge: pod-event-callback-delivery 2026-05-23 04:57:10 +09:00
767400d5c2 fix: drain snapshots before pod callbacks 2026-05-23 04:57:03 +09:00
5c4b1e1ec8 docs: add memory extract input cap ticket 2026-05-23 04:42:38 +09:00
79842b212a docs: add pod event callback delivery ticket 2026-05-23 03:29:01 +09:00
4072d35f81 docs: add spawned delegation scope reclaim ticket 2026-05-23 03:02:48 +09:00
4795b6cb4a refactor: remove legacy plural log entries 2026-05-23 02:03:42 +09:00
12d33c265c docs: track read pod output log entry bug 2026-05-23 00:53:47 +09:00
d9ca5e8c41 docs: add pod discovery restore tools ticket 2026-05-23 00:09:34 +09:00
d0cac8ab89 chore: complete spawned-registry-persist ticket 2026-05-22 23:30:16 +09:00
96fda589ac merge: spawned-registry-persist 2026-05-22 23:30:06 +09:00
1e0dc6566c feat: persist spawned pod registry 2026-05-22 23:30:02 +09:00
73e1c8332d chore: complete pod-name-resume ticket 2026-05-22 22:57:31 +09:00
292b923cae merge: pod-name-resume 2026-05-22 22:57:23 +09:00
0e562dd4d9 feat: resume pods by name 2026-05-22 22:57:16 +09:00
ca2e2352f4 chore: complete pod-state-write-points ticket 2026-05-22 22:29:23 +09:00
e0e58ebbf7 merge: pod-state-write-points 2026-05-22 22:29:12 +09:00
58608c4f57 feat: wire pod metadata lifecycle writes 2026-05-22 22:29:08 +09:00
78209d5126 chore: complete pod-state-backend ticket 2026-05-22 22:03:36 +09:00
93c91e7c06 style: run cargo fmt 2026-05-22 22:03:27 +09:00
458fdbc9e0 merge: pod-state-backend 2026-05-22 22:03:17 +09:00
8a062b1a19 feat: add pod metadata store backend 2026-05-22 22:03:11 +09:00
27be927afe Merge: live-fork-marker 2026-05-20 06:45:49 +09:00
8e1c5d3bdc chore: 空になった Storage 親見出しを TODO から削除 2026-05-20 06:45:43 +09:00
3057fe6c24 ticket: live-fork-marker 完了 2026-05-20 06:45:19 +09:00
8747cc802f chore: auto-fork ロジック二重実装を KNOWN_ISSUES に登録 2026-05-20 06:45:14 +09:00
ca7c5b82d7 ticket: live-fork-marker レビュー (Approve) 2026-05-20 06:44:54 +09:00
a9340a8817 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
c47a539278 Merge: session-grouping-introduce 2026-05-20 06:29:48 +09:00
7542605ec9 ticket: session-grouping-introduce 完了 2026-05-20 06:29:43 +09:00
0dfdd11921 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
5edc4d3b03 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
d2b3c2f53d Merge: segment-rename 2026-05-20 05:18:11 +09:00
a084324830 ticket: segment-rename 完了 2026-05-20 05:18:04 +09:00
c2b55a498b 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
2d23673393 update: Session-lifetime/scoped を Pod-lifetime に修正
タスクストア/ファイルトラッカーは compaction を跨いで Pod プロセス寿命まで生きる。
旧 SessionId = Segment の時代の表現を Pod-lifetime に正す。pod_cli の表示も Segment: に。
2026-05-20 05:06:38 +09:00
22f5d02385 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
de549812ab Merge: entry-hash-abolish 2026-05-20 04:53:52 +09:00
5362e5858c ticket: entry-hash-abolish 完了 2026-05-20 04:53:47 +09:00
55c5ac4942 update: 旧用語コメントの掃除と KNOWN_ISSUES 追記
- 残存していた head_hash / SessionHead 言及コメントを 3 箇所更新
- FsStore::read_entry_count の O(n) 計測コストを KNOWN_ISSUES に登録
2026-05-20 04:53:33 +09:00
f41c60c3ae ticket: entry-hash-abolish レビュー (Approve) 2026-05-20 04:49:17 +09:00
90e83bf2ae 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
3d091acacd 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
72c1d04cf2 Merge: invoke-turn-llmcall-semantics
# Conflicts:
#	crates/pod/src/controller.rs
2026-05-15 22:08:41 +09:00
801d7d9abb ticket: invoke-turn-llmcall-semantics 完了 2026-05-15 21:54:40 +09:00
133402dcdb ticket: worker-history-append-contract 作成 2026-05-15 21:53:24 +09:00
39a803d7e5 chore: KNOWN_ISSUES に controller_test::double_run_returns_error の flakiness を追記 2026-05-15 21:52:40 +09:00
fcc6b67f40 ticket: pod-interrupt-prep-internalize 完了 2026-05-15 21:52:24 +09:00
f90ec5ee62 ticket: pod-interrupt-prep-internalize レビュー (Approve with follow-up) 2026-05-15 21:51:57 +09:00
35988f3249 update: Paused→Run の interrupt 前処理を Pod::run に内包 2026-05-15 21:51:57 +09:00
d840add130 ticket: invoke-turn-llmcall-semantics review (Approve) 2026-05-15 21:42:43 +09:00
79b8336a14 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
fd8526799b ticket: invoke/turn/llmcall 決定事項と実装範囲を明文化 2026-05-15 06:48:57 +09:00
dcfffbcbde ticket: Exchange語撤廃、Invoke/Turn/LlmCall でセマンティクスを再整理 2026-05-15 05:41:13 +09:00
7b79743ea4 ticket: pod-input-validate-internlize完了 2026-05-15 05:38:27 +09:00
9f9e42ab59 update: Controllerで入力のValidationを行っていた部分をPod側に移す 2026-05-15 05:33:33 +09:00
a761372a9e ticket: PodとControllerの責務の抱え違いを修正するチケット 2026-05-15 04:52:39 +09:00
c4f81da828 ticket: pod-parent-turn-callback完了 2026-05-15 04:43:12 +09:00
beef5e5710 ticket: pod-parent-turn-callbackレビュー 2026-05-15 04:42:29 +09:00
d5a4f77420 ticket: 消し忘れ 2026-05-15 04:39:30 +09:00
d5a7cf2aab update: 親にターン完了を通達する経路の整理 2026-05-15 04:38:53 +09:00
35fec78519 update: エントリの単数化のフォローアップ 2026-05-14 19:42:23 +09:00
988495cfea update: 書き込みの不要なasyncを削除 2026-05-14 19:16:48 +09:00
34c89f8739 ticket: 書き込みのsync化を計画 2026-05-14 16:45:58 +09:00
f9def2d5bb update: SystemItem1本化 2026-05-14 14:36:29 +09:00
e4b66345aa ticket: イベントプロトコルと永続化におけるシステムイベントの統合 2026-05-14 04:12:40 +09:00
6358affd76 chore: cargo fmt 2026-05-14 03:36:08 +09:00
f73e648929 fix: 実態にそぐわないEvent::Entryを実装した構造を訂正 2026-05-14 03:35:52 +09:00
e7064878c2 refactor: Podのメインループのリファクタリング 2026-05-14 03:27:49 +09:00
dfa466c980 ticket: 追加:Podのメインループとソケット通信周りのリファクタリング 2026-05-13 22:16:25 +09:00
d04d2fc704 ticket: add tui manual compact command 2026-05-13 06:50:27 +09:00
3c2f5eb337 docs: update pod cli manifest flags 2026-05-13 06:44:48 +09:00
38c309535d ticket: note tui user manifest overlay mismatch 2026-05-13 06:41:23 +09:00
4a8dba276a close: complete pod manifest and file ref tickets 2026-05-13 06:30:45 +09:00
28443d2e04 merge: file-ref-directory 2026-05-13 06:30:45 +09:00
a44288c258 merge: pod-cli-manifest-flags 2026-05-13 06:30:45 +09:00
f3b99aca0c review: file-ref-directory 2026-05-13 06:30:45 +09:00
e001f4c3f9 review: pod-cli-manifest-flags 2026-05-13 06:30:45 +09:00
ba3655522b refactor: PodControllerの構造のリファクタリング 2026-05-13 06:07:38 +09:00
5a3e3f5994 docs(tickets): PodControllerの構造調整チケット作成 2026-05-13 05:43:23 +09:00
b7e329a1a1 chore: planの更新 2026-05-13 05:42:55 +09:00
524e3dc551 feat: handle directory file refs 2026-05-13 02:57:58 +09:00
b67023aafc feat: organize pod manifest cli flags 2026-05-13 02:57:50 +09:00
aa27f62409 feat: Languageインストラクションの追加 2026-05-13 02:27:30 +09:00
1803b0cf67 update: fmt + memoryに用いる言語の構成 2026-05-13 01:57:04 +09:00
93145afc3c fix: compact時にToolCallとOutputの間でCutしてしまう問題 2026-05-13 00:59:02 +09:00
d236521c77 chore: workflowの調整・knowledgeの追加テスト 2026-05-13 00:06:33 +09:00
fc4786628c merge: lint common crate 2026-05-12 21:56:49 +09:00
9cd76515d0 chore: complete lint common crate ticket 2026-05-12 21:56:39 +09:00
cf822dbc5c refactor: extract shared lint record primitives 2026-05-12 21:56:25 +09:00
bedaf62cb0 docs(tickets): submit時FileRefでディレクトリを参照した時の挙動 2026-05-12 17:39:40 +09:00
ea3014164e docs(tickets): mainfest-output-upload-limits完了 2026-05-12 17:27:47 +09:00
8d2ca5d530 feat: add manifest output upload limits 2026-05-12 16:20:15 +09:00
53b508abae Merge branch 'tui-knowledge-completion' into develop 2026-05-12 15:43:29 +09:00
27b84abccc docs(memory): fix knowledge dir path in collect_resident_knowledge doc 2026-05-12 15:07:39 +09:00
abbf7f8273 docs(tickets): review tui knowledge completion (approve) 2026-05-12 14:56:30 +09:00
2f84bd32ba feat(pod): wire knowledge slugs into # completion 2026-05-12 14:45:46 +09:00
df6ec428ca docs(tickets): tui knowledge completion unimplemented fix 2026-05-12 14:40:37 +09:00
0c34923ad1 docs(tickets): define work item query strategy 2026-05-12 02:32:32 +09:00
0af1c16009 docs(tickets): use timestamp work item ids 2026-05-12 02:07:29 +09:00
64506b643b docs: add ai maintainer work item plan 2026-05-12 01:53:52 +09:00
b78018b62e docs(tickets): add lint-common crate ticket 2026-05-12 00:06:06 +09:00
8026e8e319 merge: workflow crate extraction 2026-05-11 22:50:19 +09:00
7ac948afa0 docs(tickets): complete workflow crate extraction 2026-05-11 22:50:06 +09:00
96821556c6 review: workflow crate extraction 2026-05-11 22:49:50 +09:00
f70975789e refactor: extract workflow crate 2026-05-11 22:49:07 +09:00
520895f1c9 merge: anthropic assistant burst bundling 2026-05-11 22:24:36 +09:00
072c1bfbc7 docs(tickets): complete anthropic assistant burst bundling 2026-05-11 22:23:53 +09:00
fdb181e825 review: anthropic assistant burst bundling 2026-05-11 22:23:38 +09:00
cfb7f09e38 fix: bundle anthropic assistant bursts 2026-05-11 22:22:36 +09:00
1676e525e8 merge: memory usage metrics 2026-05-11 21:46:24 +09:00
01bdf04f2e docs(tickets): complete memory usage metrics 2026-05-11 21:46:19 +09:00
96d5be4337 review: memory usage metrics 2026-05-11 21:46:19 +09:00
6c93ec38df feat: add memory usage event metrics 2026-05-11 21:29:48 +09:00
1d8db0aadd docs(tickets): complete memory phase naming cleanup 2026-05-11 17:16:36 +09:00
69ac3799d6 docs(tickets): simplify memory usage metrics 2026-05-11 16:54:23 +09:00
eb9a67decc fix: remove remaining memory phase wording 2026-05-11 01:57:39 +09:00
0e906b72b7 docs(tickets): compact-worker-occupancy-cap完了 2026-05-11 01:56:20 +09:00
a2aecbf029 update: memoryシステムの"Phase"表記を撤廃 2026-05-11 01:55:28 +09:00
a8a6e049bc docs(tickets): memory-extract-occupancy-cap 完了 2026-05-11 01:32:45 +09:00
dbc96ee075 review: memory-extract-occupancy-cap (approve) 2026-05-11 01:25:20 +09:00
2c8aec5385 feat: extract worker サーキットブレーカーを占有量ベースに統一 2026-05-11 01:20:37 +09:00
eb9af32b49 docs(tickets): add memory-extract-occupancy-cap ticket 2026-05-11 01:14:59 +09:00
919602b496 Merge branch 'compact-worker-occupancy-cap' into develop 2026-05-11 01:12:32 +09:00
507b164822 review: compact-worker-occupancy-cap (set_max_turns 分岐削除) 2026-05-11 00:56:41 +09:00
8100a5dfd1 feat: compact worker サーキットブレーカーを占有量ベースに統一 2026-05-11 00:43:16 +09:00
8f3c1942fb docs(tickets): add memory audit log ticket 2026-05-11 00:06:42 +09:00
d6f27f7c45 docs(tickets): completed tickets cleanup 2026-05-10 17:31:34 +09:00
b7953b3d28 merge: memory prompt record policy 2026-05-10 14:40:58 +09:00
20c82a4bf6 docs: generalize memory prompt record policy 2026-05-10 14:40:52 +09:00
7d9b9682bd docs: memory effectiveness plan 2026-05-10 01:25:10 +09:00
75ade5750e docs: memory prompt ticket policy ticket 2026-05-10 01:13:57 +09:00
3def5edbdf feat: client-crateの実装 2026-05-10 00:57:50 +09:00
df0be1cd6b chore: E2Eの計画とgit運用の話 2026-05-09 05:04:57 +09:00
175343c612 docs(tickets): file-ref-symlink-diagnostics完了 2026-05-09 04:22:27 +09:00
4d6b548611 docs(tickets): file-ref-symlink-diagnosticsレビュー 2026-05-09 04:21:56 +09:00
f479aa5206 feat: Toolsのシンボリックリンク対応 2026-05-09 04:21:56 +09:00
94c2f3a106 docs(tickets): tui-assistant-markdown完了 2026-05-09 03:31:49 +09:00
45b9912c8f docs(tickets): permission既定policy整理チケット追加 2026-05-09 03:27:22 +09:00
7c5c1609cb docs(tickets): permission-extension-point完了 2026-05-09 03:20:17 +09:00
60144c550a feat: パターンベースのツール権限制御を追加 2026-05-09 03:20:02 +09:00
2df9de73c7 chore: tui compact progress ticket完了 2026-05-09 03:14:23 +09:00
41bce21339 feat: compactのプログレス表示 2026-05-09 03:11:53 +09:00
584cbe406a chore: git方針の変更とセマンティクス変更の計画の帳尻合わせ 2026-05-08 20:17:11 +09:00
0900e05f9e docs(tickets): 自己改善workflowの設計 2026-05-08 01:50:55 +09:00
d036d53096 docs(tickets): workflow-directory-layout完了 2026-05-08 01:08:25 +09:00
69edd29a46 update: Workflowディレクトリ修正のフォローアップ 2026-05-08 00:59:08 +09:00
902b8d759b feat: Workflowの読み取り位置変更の実装 2026-05-08 00:15:50 +09:00
22d87f8ade docs(tickets): reportの運用・Workflowのディレクトリ位置修正 2026-05-07 23:34:00 +09:00
6c95b2da56 feat: TUIのmarkdown対応 2026-05-05 18:30:25 +09:00
fc5cfefb62 docs(tickets): PermissionのチケットとTUIのmd表示 2026-05-05 17:16:03 +09:00
ba2c8ae687 docs(tickets): agent-skills完了 2026-05-05 16:00:40 +09:00
9d709c6470 update: Agent skills実装のレビュー・対応 2026-05-05 13:54:02 +09:00
dec17c9909 feat: writingに対する基本的な指示promptを追加 2026-05-05 13:42:34 +09:00
37065144da feat: agent skillsの互換実装 2026-05-05 13:16:10 +09:00
5acb0d4d85 fix: Reasoningの永続化のスキーマのミスを修正 2026-05-05 12:30:29 +09:00
d0270288de docs(tickets): turnのセマンティクスを変える計画 2026-05-05 12:29:52 +09:00
0d5ec4ff63 docs(tickets): reasoning-history-perisit完了 2026-05-04 23:06:21 +09:00
75c573fed1 update: Reasoningコンテキスト管理のレビュー・対応 2026-05-04 23:05:08 +09:00
594671edc3 feat: Reasoningのコンテキスト管理の対応 2026-05-04 21:31:44 +09:00
e31cbcb150 docs(tickets): Reasoningのコンテキスト管理とPruneの調整チケット追加 2026-05-04 21:16:31 +09:00
9f14a43a59 docs(tickets): tui-task-display完了 2026-05-04 20:43:21 +09:00
f33eba3fe6 feat: Task表示のレビュー・修正 2026-05-04 17:28:39 +09:00
cb7da11de7 feat: TUI上に進行中のTaskを表示する実装 2026-05-04 17:06:02 +09:00
006897790c docs(tickets): Compaction進行中のライブ表示 2026-05-04 17:03:51 +09:00
cb72533ab0 docs(tickets): post-run memory detach 完了 2026-05-04 16:11:38 +09:00
954cf200e2 feat: Pos処理の非同期化・Busy状態の削除 2026-05-04 15:52:27 +09:00
5b38aa6a87 docs(tickets): 追加:タスクリストの表示とコンテキスト長インジケータ 2026-05-04 15:32:40 +09:00
ef7d7bd6a1 docs(tickets): Busyの切り離し 2026-05-04 13:20:25 +09:00
a61b498564 Merge branch 'llm-worker-transient-retry' into develop 2026-05-04 13:16:26 +09:00
dcd22d4399 docs(tickets): pod状態のTUI同期完了 2026-05-04 13:08:44 +09:00
f2dda9097e feat: Podのステータス同期の修正 2026-05-04 12:55:29 +09:00
560c23bc75 feat: Podのステータスを厳密にし、同期漏れを防ぐ 2026-05-04 12:55:11 +09:00
79e85ccda6 docs(tickets): llm-worker-transient-retry完了 2026-05-04 12:51:41 +09:00
73a9efdc9a 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
19df6340cd 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
a0771608b1 Merge branch 'tui-system-message-render' into develop 2026-05-04 12:10:17 +09:00
b4ca718c24 docs(tickets): tui-system-message-render完了 2026-05-04 12:05:50 +09:00
a0e1583916 feat: システムメッセージをTUIで表示させる 2026-05-04 12:04:09 +09:00
0e2521d7c1 update: Taskツールの説明を更新 2026-05-04 11:32:04 +09:00
798b95a887 docs(tickets): tuiトークン表示完了 2026-05-04 00:07:59 +09:00
59b953c139 docs(tickets): tuiトークン表示レビュー 2026-05-04 00:05:59 +09:00
e6b6df09b8 feat: tuiのトークン集計表示の修正 2026-05-04 00:01:37 +09:00
9a06494bc6 docs(tickets): TUI表示トークンの集計の修正 2026-05-03 23:28:31 +09:00
58b12d859b docs(tickets): チケット追加:システムメッセージのTUI表示とセッションのロールバック・フォーク 2026-05-03 22:43:21 +09:00
aec7a071ac docs(tickets): tui-pod-event-render 完了 (消し忘れ片付け) 2026-05-03 22:14:24 +09:00
a18f74ca24 update: Taskツール群の説明を更新 2026-05-03 22:09:45 +09:00
8d9910fd20 docs(tickets): notify-history-persist 完了 (消し忘れ片付け) 2026-05-03 22:07:18 +09:00
698175e60c docs(tickets): session-todo-reminder spec を pending_history_appends に改訂 (AGENTS.md 揮発禁止に整合) 2026-05-03 21:53:20 +09:00
9ffe91b5bc Merge branch 'session-todo-tools' into develop
# Conflicts:
#	tickets/session-todo.md
2026-05-03 21:50:30 +09:00
4dae614a55 docs(tickets): session-todo (本体) 完了 2026-05-03 21:48:44 +09:00
b22b226fea update: tuiからspawnする際にエラー詳細が落ちていた問題を修正 2026-05-03 21:47:54 +09:00
25d07976f2 docs(tickets): notify-history-persist完了 2026-05-03 21:37:13 +09:00
6021f27469 docs(tickets): session-todo レビュー反映 (Approve) + reminder spec 段階レビュー 2026-05-03 21:34:54 +09:00
2e8dd759d8 fix: TaskStore snapshot を JSON ブロック化 + 構造ラウンドトリップテスト追加 2026-05-03 21:33:50 +09:00
9679661313 fix: TaskStore snapshot を compact 後 history の末尾に置いて retained 中の TaskCreate 重複を防ぐ 2026-05-03 21:26:49 +09:00
9e58149dda feat: notify-history-persist実装 2026-05-03 19:27:22 +09:00
3cbd759397 feat: セッション内 Task ツール (TaskCreate/List/Get/Update + 履歴 replay + compact 跨ぎ) 2026-05-03 19:03:52 +09:00
b8c459549f docs(tickets): セッション内 Task ツールを本体と注意機構に分割 2026-05-03 19:03:48 +09:00
7ddfdb09b5 Merge branch 'resume-scope-claim' into develop
# Conflicts:
#	TODO.md
2026-05-03 18:59:01 +09:00
613bf07610 docs(tickets): resume-scope-claim 完了 2026-05-03 18:56:39 +09:00
b90291d5a0 fix: resume-scope-claim レビュー指摘対応 (deny セマンティクス doc・破損 snapshot の警告ログ) 2026-05-03 18:56:21 +09:00
364f936ed1 docs(tickets): resume-scope-claim レビュー (Approve) 2026-05-03 18:46:15 +09:00
3fe4f169b7 docs(tickets): Notifyが永続化されいない問題についてのチケット 2026-05-03 18:45:10 +09:00
4e48f35e55 feat: resume時のscope claimを過去の有効scopeに揃える 2026-05-03 17:12:36 +09:00
aca5bda1f2 feat: session-metrics完了 2026-05-03 15:56:06 +09:00
70c4f1930e feat: session-metrics実装 2026-05-03 15:10:43 +09:00
702ed79517 feat: TUIに他Podからの通知を表示する 2026-05-03 12:45:05 +09:00
b35bb2154f docs(tickets): 消し忘れチケットども 2026-05-03 01:16:22 +09:00
56e94911c1 chore: TODOから[ ]を削除 2026-05-03 01:08:43 +09:00
201bb6c82e Update AGENTS.md 2026-05-03 01:06:23 +09:00
4466a35d6c docs(tickets): tuiでPodEventを表示する・セッション中でメトリクスを取るチケットを追加 2026-05-03 01:01:09 +09:00
2c86e64f19 update: tuiの文字入力のCtrlブロックを追加 2026-05-03 00:44:38 +09:00
261c682e5e update: memoryシステム周りのプロンプトの整理 2026-05-03 00:27:10 +09:00
3bb8af824e docs(tickets): memory-consolidation-drop-input-cap完了 2026-05-02 23:57:36 +09:00
689a988e83 update: Consolidationの不要なToken上限の削除 2026-05-02 23:48:33 +09:00
9d0b9e9d90 docs(tickets): セッション内TODOツールと注意機構のチケット 2026-05-02 23:48:01 +09:00
81ff4c6073 update: codexのキャッシュ利用が出来てなかった問題 2026-05-02 03:23:44 +09:00
cb5cd1e3d1 fix: tuiからのPod作成の挙動を修正・開発時にcargo runでpodを起動する経路を実装 2026-05-02 02:13:30 +09:00
08da79822d 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
1b8a9efdd5 docs(tickets): workflow完了 2026-05-02 01:40:06 +09:00
76e5e66326 update: workflowの実装修正 2026-05-02 01:38:50 +09:00
b8aab6725b docs(tickets): 消し忘れチケット 2026-05-02 01:36:19 +09:00
e9b426b825 feat: dynamic-scopeの実装修正 2026-05-02 01:33:32 +09:00
d33b1c111e feat: dynamic-scopeの実装 2026-05-02 01:26:17 +09:00
5479b14411 fix: SpawnPodの起動経路の問題・を修正 2026-05-02 01:09:57 +09:00
e2d1fa120f Implement workflow MVP 2026-05-02 00:46:47 +09:00
8e24b3c607 update: manifestで一部値のzeroの扱いを変更 2026-05-02 00:08:46 +09:00
573501e37c fix(llm-worker): openai_responsesのroleの最新の投影を反映 2026-05-01 23:55:26 +09:00
9ac127d607 chore: dev-depsの整理 2026-05-01 23:50:14 +09:00
7899ab4386 docs(tickets): workflowのプロパティ名の修正 2026-05-01 23:40:47 +09:00
99dc94416b chore: 依存パッケージの集約 2026-05-01 23:35:46 +09:00
469c5ead99 Merge branch 'tui-mouse-scroll' into develop 2026-05-01 23:22:58 +09:00
bccd60d9be feat: memory P2の修正 2026-05-01 23:22:49 +09:00
201e68f17e feat(tui): マウスホイールスクロール完了 2026-05-01 23:16:02 +09:00
b907715dd4 スキルの整理 2026-05-01 23:14:37 +09:00
06f4dc0428 feat(tui): マウスホイールでスクロールする実装 2026-05-01 23:14:16 +09:00
d8a7200ea4 メモリPhase2の実装 2026-05-01 23:00:55 +09:00
f1b7af6249 docs: memoryシステムの仕様変更と、動的Tool・VCSの話 2026-05-01 18:47:52 +09:00
57fab557d3 bashツール一旦完了 2026-05-01 18:47:09 +09:00
8773e751ec bashツール実装 2026-05-01 18:14:13 +09:00
5b27fb9e19 ClaudeによるTool出力メタ認知 2026-05-01 02:47:44 +09:00
d7bc7ab3dd ファイル参照を与えた際に自動的に読ませる実装 2026-04-30 21:58:10 +09:00
bc81dc1513 TUI補完の細かい挙動修正 2026-04-30 14:38:03 +09:00
623b54cefc tuiの補完の実装 2026-04-30 12:46:48 +09:00
f914ae235a claudeの動的ツールの調査レポート 2026-04-30 01:35:42 +09:00
0df59462ea fix: セッション復元時にhistoryが表示されない問題 2026-04-30 00:02:26 +09:00
f31c58dccd cargo fmt 2026-04-29 23:20:25 +09:00
3fc65e6f6b templatureがcodexエンドポイントで使えない件の修正 2026-04-29 23:20:16 +09:00
b0393d2fe9 session-log関連完了 2026-04-29 23:00:55 +09:00
fa68957277 session-logリファクタのレビュー・修正 2026-04-29 22:55:36 +09:00
e3b36371e9 session-log-segments実装 2026-04-29 22:42:10 +09:00
8a9e3b4fe3 session-log-decouple-item実装 2026-04-29 22:24:18 +09:00
913ee4764a session-storeの永続化形式からllm-workerの内部型を削除 2026-04-29 22:09:30 +09:00
7736baaec9 tui-input-word-motion完了 2026-04-29 21:45:49 +09:00
b0db02da6d tui-input-word-motionレビュー・半角カナに関する修正 2026-04-29 21:41:24 +09:00
b79747bd0c tuiの単語単位Backspace 2026-04-29 21:31:19 +09:00
99dbb1c6c0 tuiの単語境界カーソル移動実装 2026-04-29 21:23:29 +09:00
dfb458cff2 workflowのチケットとtuiの単語境界カーソル移動のチケット 2026-04-29 21:22:49 +09:00
81ac7ccb7b pod-registry-rename完了 2026-04-29 21:05:09 +09:00
0b322a645a pod-registry-rename修正 2026-04-29 21:04:47 +09:00
274b7df32d pod-registryのモジュール分割 2026-04-29 20:14:34 +09:00
8a8fd225bf scope-lock -> pod-registry 2026-04-29 20:01:32 +09:00
1aa06dba60 scope.lockの意味変更に伴うクレート名変更チケット作成 2026-04-29 19:54:08 +09:00
deab5c5b50 memory-phase1-extract完了消し忘れ 2026-04-29 19:53:37 +09:00
59bbfb9621 tui-session-restore完了 2026-04-29 19:52:24 +09:00
fd96a517bb tuiからセッションを復帰する経路の実装 2026-04-29 19:03:03 +09:00
87768c2e2d 不要なforkの削除 2026-04-28 20:19:50 +09:00
2b89bb6d2e resumeの実装 2026-04-28 18:52:58 +09:00
023ed09adc max_tokenとreasoning_tokenに関するdocs修正 2026-04-28 18:01:17 +09:00
ce4c0930c3 max_tokensのスキーマ不整合に関する修正 2026-04-28 17:58:24 +09:00
3d1b8a4761 tui-thinking-display完了 2026-04-28 16:23:09 +09:00
f385f06abc tui-thinking-display修正 2026-04-28 16:22:45 +09:00
cf4c454a03 TUIにThinkingを表示する実装 2026-04-28 16:10:48 +09:00
513653ce55 ThinkingのTUI表示のチケット作成 2026-04-28 16:07:41 +09:00
1e65287bf1 session-store-llm-worker-type-ownership完了 2026-04-28 15:44:16 +09:00
6fe19b84ce セッション関連の責務の分離 2026-04-28 15:43:34 +09:00
e49fb3f1a0 memory-phase1の、トークンカウントの実装位置が悪い件 2026-04-28 14:24:38 +09:00
e437028849 memory-phase1-extract修正 2026-04-28 13:12:21 +09:00
3d04f793de memoryを抽出する仕組みの実装 2026-04-28 12:58:33 +09:00
141b77b7e4 session-restoreの設計更新 2026-04-28 12:42:49 +09:00
37d35df6be session復帰経路を作るチケット・テスト用のファイルの削除 2026-04-28 12:31:38 +09:00
8cf1d6c9cf memoryが.insomnia配下ではなくworkspace root直下を想定していた問題の修正 2026-04-28 11:53:08 +09:00
dfa6213c18 memoryのクエリと動作のテスト 2026-04-28 11:37:41 +09:00
4273d2a463 worker-generation-settings完了 2026-04-28 09:38:23 +09:00
bdf2a08459 生成設定のmanifest化の実装 2026-04-28 09:37:22 +09:00
7a0ed7d744 cargo fmt 2026-04-27 22:51:07 +09:00
bcaa4645f7 model-reasoning-control完了 2026-04-27 22:49:56 +09:00
84c6c2f17f model-reasoning-contolレビュー 2026-04-27 22:41:51 +09:00
0435ec5cbd model-reasoning-control実装 2026-04-27 22:25:27 +09:00
c75efb50b9 home-dir-layout完了 2026-04-27 22:11:15 +09:00
8cf0bd6374 home-dir-layout修正 2026-04-27 22:10:36 +09:00
8658845b02 home-dirの整理 2026-04-27 21:45:30 +09:00
306b1cf942 reasoningを利用可能にするチケット 2026-04-27 20:21:22 +09:00
695cfa05a7 memory-resident-injection完了 2026-04-27 18:30:21 +09:00
e8559d4bee メモリー内容のシステムプロンプトへの埋め込みの実装 2026-04-27 18:25:47 +09:00
12c1d55127 環境変数に関するチケットの修正 2026-04-27 18:11:40 +09:00
bc9eb3aa1c pod-spawn-ui完了・設定UI関連のチケット作成 2026-04-27 17:38:32 +09:00
978d542855 memory-search-tool完了 2026-04-27 17:26:07 +09:00
c9d1d4fa5c memoryサーチツールを実装 2026-04-27 17:24:08 +09:00
c8fe95901c manifest読み込み経路の整理チケット作成 2026-04-27 17:17:00 +09:00
9f1443b027 manifest側で設定ファイルの収集を行うようにした 2026-04-27 16:52:23 +09:00
c4ca9e1d89 tuiからSpawnする仮UI 2026-04-27 16:22:06 +09:00
cd8b620e6a memory-file-format完了 2026-04-27 13:59:04 +09:00
f2e47629d0 メモリーに関するクレート作成・ファイル構造の実装 2026-04-27 13:33:31 +09:00
c7a873bcf9 セグメントのセッション永続化チケット 2026-04-27 13:25:16 +09:00
c3278bb8da submit-segment-protocol完了 2026-04-27 11:42:42 +09:00
0a3af686f7 submitをvec segmentを受け付ける形に変更 2026-04-27 11:03:58 +09:00
c9a7d652dc notification-naming完了 2026-04-26 23:30:46 +09:00
2ee536ed71 Method::NotifyとEvent::Notificationが紛らわしい問題 2026-04-26 23:25:50 +09:00
82f08b966b memory実装チケット 2026-04-26 17:00:38 +09:00
d3b729c671 カタログの実装完了、ドキュメント整理 2026-04-24 13:33:56 +09:00
0a97886005 podのモジュール分割完了 2026-04-24 11:58:11 +09:00
4763173f36 podのモジュール分割 2026-04-24 11:48:27 +09:00
30f9abacb8 modelsとprovidersをカタログ化 2026-04-24 10:45:03 +09:00
7ecb1e6fc1 モデルとプロバイダーをカタログ化するチケット 2026-04-23 16:18:30 +09:00
ccf1f2b6bf llm-provider-catalog実装 2026-04-23 15:37:51 +09:00
e21d2041ef Agents.mdを一定閾値でturncateする仕様を削除 2026-04-23 01:34:25 +09:00
26ce346a81 pod-prompt-catalog完了 2026-04-22 17:43:42 +09:00
b8d368f5e5 Promptを一元管理するファイルから参照する実装 2026-04-22 17:43:05 +09:00
0c1276b730 Memoryシステムの整理・Promptカタログチケット 2026-04-22 13:21:15 +09:00
270d7923ab TUIのEditツール周りの表示とカラー 2026-04-22 01:17:58 +09:00
ef294eeb68 複数クライアント間でのRunメソッドの同期漏れ 2026-04-21 23:59:49 +09:00
d3ba0a299a 改行テキストの行計算・Padding設定 2026-04-21 23:26:34 +09:00
72128aab9f TUIのオーバーホール実装 2026-04-21 23:12:35 +09:00
388079759c protocol-tool-result-shape完了 2026-04-21 20:52:19 +09:00
ce59c5320e TUIに向けたprotocolの詳細調整 2026-04-21 20:50:59 +09:00
de3272fdfd TUIオーバーホールチケット 2026-04-21 19:37:14 +09:00
ca5a3d1152 メモリシステムの設計 2026-04-21 19:23:07 +09:00
47da4a03cb モデル性能のハードコードを消し飛し、Codexのフォーマットの修正 2026-04-21 18:35:56 +09:00
2914800673 Docsのアップロード 2026-04-21 17:39:43 +09:00
62 changed files with 3725 additions and 483 deletions

View File

@ -1,4 +1,4 @@
//! pod バイナリをサブプロセスとして立ち上げ、`INSOMNIA-READY` を待つ
//! `insomnia-pod` バイナリをサブプロセスとして立ち上げ、`INSOMNIA-READY` を待つ
//! ハンドシェイク。
//!
//! - 親プロセス (TUI / GUI / E2E) は overlay TOML を組み立ててこの関数に
@ -258,7 +258,7 @@ async fn drain_stderr_into_tail(stderr_path: &Path, tail: &mut StderrTail, offse
}
/// Resolves the binary used to launch a child Pod. Must point at a
/// `pod`-compatible executable — the parent reads the child's stderr
/// `insomnia-pod`-compatible executable — the parent reads the child's stderr
/// directly looking for `INSOMNIA-READY`, so any wrapper that emits
/// extra lines on stderr will pollute that handshake.
///
@ -271,7 +271,7 @@ fn resolve_pod_command() -> PathBuf {
{
return PathBuf::from(cmd);
}
PathBuf::from("pod")
PathBuf::from("insomnia-pod")
}
struct StderrTail {

View File

@ -4,6 +4,10 @@ version = "0.1.0"
edition.workspace = true
license.workspace = true
[[bin]]
name = "insomnia-pod"
path = "src/main.rs"
[dependencies]
async-trait = { workspace = true }
clap = { version = "4.6.0", features = ["derive"] }

View File

@ -23,7 +23,8 @@ 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, RunResult, Segment, TurnResult,
AlertLevel, AlertSource, ErrorCode, Event, Method, PodStatus, RewindTargetId, RunResult,
Segment, TurnResult,
};
// ---------------------------------------------------------------------------
@ -781,6 +782,45 @@ 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;
@ -1014,10 +1054,10 @@ where
message: "Pod is already executing a turn".into(),
});
}
Some(Method::Compact) => {
Some(Method::Compact | Method::ListRewindTargets | Method::RewindTo { .. }) => {
let _ = event_tx.send(Event::Error {
code: ErrorCode::AlreadyRunning,
message: "Pod is already executing a turn; compact can only run while idle"
message: "Pod is already executing a turn; rewind/compact can only run while idle or paused"
.into(),
});
}
@ -1069,6 +1109,70 @@ 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

@ -596,7 +596,7 @@ fn resolve_pod_command() -> PathBuf {
{
return PathBuf::from(cmd);
}
PathBuf::from("pod")
PathBuf::from("insomnia-pod")
}
#[derive(Debug, Deserialize, JsonSchema)]

View File

@ -9,7 +9,7 @@ use session_store::{FsStore, PodMetadataStore, SegmentId, Store};
#[derive(Debug, Parser)]
#[command(
name = "pod",
name = "insomnia-pod",
about = "Spawn a Pod process from manifest layers or a single manifest file"
)]
struct Cli {
@ -364,19 +364,25 @@ permission = "write"
#[test]
fn user_manifest_flag_is_not_accepted() {
let err = Cli::try_parse_from(["pod", "--user-manifest", "manifest.toml"]).unwrap_err();
let err =
Cli::try_parse_from(["insomnia-pod", "--user-manifest", "manifest.toml"]).unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument);
}
#[test]
fn manifest_conflicts_with_project_and_overlay() {
let project_err =
Cli::try_parse_from(["pod", "--manifest", "manifest.toml", "--project", "."])
.unwrap_err();
let project_err = Cli::try_parse_from([
"insomnia-pod",
"--manifest",
"manifest.toml",
"--project",
".",
])
.unwrap_err();
assert_eq!(project_err.kind(), clap::error::ErrorKind::ArgumentConflict);
let overlay_err = Cli::try_parse_from([
"pod",
"insomnia-pod",
"--manifest",
"manifest.toml",
"--overlay",
@ -391,7 +397,8 @@ permission = "write"
let tmp = TempDir::new().unwrap();
let manifest = tmp.path().join("manifest.toml");
write(&manifest, &manifest_toml("single", tmp.path()));
let cli = Cli::try_parse_from(["pod", "--manifest", manifest.to_str().unwrap()]).unwrap();
let cli = Cli::try_parse_from(["insomnia-pod", "--manifest", manifest.to_str().unwrap()])
.unwrap();
let err = resolve_manifest_with_user_manifest_env(&cli, Some(OsString::from("user.toml")))
.unwrap_err();
@ -405,7 +412,8 @@ permission = "write"
let tmp = TempDir::new().unwrap();
let manifest = tmp.path().join("manifest.toml");
write(&manifest, &manifest_toml("single", tmp.path()));
let cli = Cli::try_parse_from(["pod", "--manifest", manifest.to_str().unwrap()]).unwrap();
let cli = Cli::try_parse_from(["insomnia-pod", "--manifest", manifest.to_str().unwrap()])
.unwrap();
let (manifest, loader) =
resolve_manifest_with_user_manifest_env(&cli, Some(OsString::new())).unwrap();
@ -422,8 +430,12 @@ permission = "write"
write(&user_manifest, &manifest_toml("from-env", tmp.path()));
let no_project_root = tmp.path().join("no-project");
std::fs::create_dir_all(&no_project_root).unwrap();
let cli =
Cli::try_parse_from(["pod", "--project", no_project_root.to_str().unwrap()]).unwrap();
let cli = Cli::try_parse_from([
"insomnia-pod",
"--project",
no_project_root.to_str().unwrap(),
])
.unwrap();
let (manifest, _loader) = resolve_manifest_with_user_manifest_env(
&cli,
@ -438,8 +450,8 @@ permission = "write"
fn pod_flag_conflicts_with_session() {
let segment_id = session_store::new_segment_id();
let segment_id = segment_id.to_string();
let err =
Cli::try_parse_from(["pod", "--pod", "agent", "--session", &segment_id]).unwrap_err();
let err = Cli::try_parse_from(["insomnia-pod", "--pod", "agent", "--session", &segment_id])
.unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
@ -449,7 +461,7 @@ permission = "write"
let manifest = tmp.path().join("manifest.toml");
write(&manifest, &manifest_toml("from-file", tmp.path()));
let cli = Cli::try_parse_from([
"pod",
"insomnia-pod",
"--manifest",
manifest.to_str().unwrap(),
"--pod",
@ -471,7 +483,7 @@ permission = "write"
&manifest_toml("unused", tmp.path()).replace("name = \"unused\"\n", ""),
);
let cli = Cli::try_parse_from([
"pod",
"insomnia-pod",
"--manifest",
manifest.to_str().unwrap(),
"--pod",
@ -491,8 +503,12 @@ permission = "write"
write(&single_manifest, &manifest_toml("single-file", tmp.path()));
std::fs::create_dir_all(tmp.path().join("prompts")).unwrap();
std::fs::create_dir_all(tmp.path().join(".insomnia").join("prompts")).unwrap();
let cli =
Cli::try_parse_from(["pod", "--manifest", single_manifest.to_str().unwrap()]).unwrap();
let cli = Cli::try_parse_from([
"insomnia-pod",
"--manifest",
single_manifest.to_str().unwrap(),
])
.unwrap();
let (manifest, loader) = resolve_manifest_with_user_manifest_env(&cli, None).unwrap();

View File

@ -40,7 +40,9 @@ 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, Segment};
use protocol::{
AlertLevel, AlertSource, Event, RewindSummary, RewindTarget, RewindTargetId, Segment,
};
use tokio::sync::broadcast;
use tokio::task::JoinHandle;
@ -830,6 +832,85 @@ 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(());
@ -4328,6 +4409,110 @@ 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)]
@ -4809,6 +4994,156 @@ 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

@ -507,6 +507,18 @@ mod tests {
assert!(rendered.contains("mark_read_required"));
}
#[test]
fn internal_worker_prompts_do_not_include_default_memory_guidance() {
let cat = PromptCatalog::builtins_only().unwrap();
let compact = cat.compact_system().unwrap();
let extract = cat.memory_extract_system("Japanese").unwrap();
let consolidate = cat.memory_consolidation_system("Japanese").unwrap();
for rendered in [compact, extract, consolidate] {
assert!(!rendered.contains("### Memory and knowledge"));
assert!(!rendered.contains("Do not query memory every turn"));
}
}
#[test]
fn memory_worker_prompts_include_language() {
let cat = PromptCatalog::builtins_only().unwrap();

View File

@ -440,7 +440,11 @@ mod tests {
let rendered = tmpl
.render(&ctx(dir.path(), &scope, vec!["Read".into()], None))
.unwrap();
// Builtin default body must expose the language policy.
// Builtin default body must expose the tool and language policies.
assert!(rendered.contains("### Memory and knowledge"));
assert!(rendered.contains("MemoryQuery"));
assert!(rendered.contains("MemoryRead(kind=summary)"));
assert!(rendered.contains("Do not query memory every turn"));
assert!(rendered.contains("## Language"));
assert!(rendered.contains("`language`: `match the user's language"));
// Trailing section must be present.

View File

@ -2,7 +2,7 @@
//!
//! Wires pod-registry delegation, overlay-TOML construction, subprocess
//! launch, and socket handoff into a single `Tool` implementation. When
//! the LLM calls `SpawnPod`, a fresh `pod` binary is exec'd in its own
//! the LLM calls `SpawnPod`, a fresh `insomnia-pod` binary is exec'd in its own
//! process group, the pod-registry is updated atomically, and the child's
//! first turn is kicked off by handing its socket a `Method::Run`.
@ -303,7 +303,8 @@ impl SpawnPodTool {
overlay_toml: &str,
predicted_socket: &Path,
) -> Result<(), ToolError> {
let pod_command = std::env::var("INSOMNIA_POD_COMMAND").unwrap_or_else(|_| "pod".into());
let pod_command =
std::env::var("INSOMNIA_POD_COMMAND").unwrap_or_else(|_| "insomnia-pod".into());
// Pre-create the child's runtime dir so we have a stable place to
// capture its stderr before it has had a chance to bind anything.
@ -381,7 +382,7 @@ fn parse_scope(rules: &[ScopeRuleInput]) -> Result<Vec<ScopeRule>, ToolError> {
.collect()
}
/// Serialise the overlay TOML that gets handed to the child `pod`
/// Serialise the overlay TOML that gets handed to the child `insomnia-pod`
/// binary via `--overlay`. `PodManifestConfig`'s `Serialize` impl is
/// the single source of truth for the on-disk manifest format.
///

View File

@ -2,7 +2,7 @@
//!
//! These tests exercise the tool's pod-registry delegation, subprocess
//! launch, socket handoff, and `spawned_pods.json` write without relying
//! on the real `pod` binary. `INSOMNIA_POD_COMMAND` is pointed at
//! on the real `insomnia-pod` binary. `INSOMNIA_POD_COMMAND` is pointed at
//! `/bin/true` (which exits immediately) while a test-owned Unix
//! listener pre-binds the predicted socket path, so the tool sees the
//! "child" as live.

View File

@ -36,6 +36,14 @@ 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.
///
@ -125,7 +133,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, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Segment {
/// Free-form text. The fallback every client can produce.
@ -433,6 +441,19 @@ 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.
@ -545,6 +566,34 @@ 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

@ -4,6 +4,10 @@ version = "0.1.0"
edition.workspace = true
license.workspace = true
[[bin]]
name = "insomnia"
path = "src/main.rs"
[dependencies]
client = { workspace = true }
protocol = { workspace = true }

View File

@ -2,20 +2,29 @@ use std::collections::VecDeque;
use std::time::Instant;
use protocol::{
AlertLevel, AlertSource, CompletionEntry, CompletionKind, Event, Method, PodStatus, RunResult,
Segment,
AlertLevel, AlertSource, CompletionEntry, CompletionKind, Event, Method, PodStatus,
RewindTarget, RunResult, Segment,
};
use crate::block::{
Block, CompactEvent, ThinkingBlock, ThinkingState, ToolCallBlock, ToolCallState,
};
use crate::cache::FileCache;
use crate::command::{CommandEnvironment, CommandExecution, CommandInputMode, CommandRegistry};
use crate::command::{
CommandCandidate, 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
@ -42,6 +51,38 @@ 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>,
@ -99,6 +140,7 @@ 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
@ -116,6 +158,10 @@ 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
@ -158,6 +204,7 @@ 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(),
@ -166,6 +213,9 @@ 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,
@ -910,6 +960,54 @@ 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 { .. } => {}
@ -1078,26 +1176,213 @@ 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<crate::command::CommandCandidate> {
pub fn command_suggestions(&self) -> Vec<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,
@ -1119,9 +1404,16 @@ 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
}
@ -1146,23 +1438,40 @@ 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();
@ -1193,6 +1502,7 @@ 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,6 +147,15 @@ 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
}
@ -284,6 +293,15 @@ 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(
@ -303,6 +321,20 @@ 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 {
@ -350,6 +382,18 @@ 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::*;
@ -421,4 +465,40 @@ 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,6 +144,8 @@ 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,
}
@ -153,6 +155,7 @@ impl Default for InputBuffer {
Self {
atoms: Vec::new(),
cursor: 0,
scroll_offset: 0,
next_paste_id: 1,
}
}
@ -166,6 +169,7 @@ impl InputBuffer {
pub fn clear(&mut self) {
self.atoms.clear();
self.cursor = 0;
self.scroll_offset = 0;
}
pub fn is_empty(&self) -> bool {
@ -697,8 +701,27 @@ 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
@ -746,6 +769,119 @@ 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

@ -44,6 +44,8 @@ use crate::app::App;
use crate::picker::PickerOutcome;
use crate::spawn::{SpawnOutcome, SpawnReady};
type FullscreenTerminal = Terminal<CrosstermBackend<io::Stdout>>;
fn resolve_socket(pod_name: &str, override_path: Option<PathBuf>) -> PathBuf {
if let Some(p) = override_path {
return p;
@ -59,20 +61,20 @@ fn resolve_socket(pod_name: &str, override_path: Option<PathBuf>) -> PathBuf {
#[derive(Debug)]
enum Mode {
Spawn,
/// `tui <name>` / `tui --pod <name>`: attach to a live Pod by name if
/// possible; otherwise launch `pod --pod <name>` so the pod process
/// `insomnia <name>` / `insomnia --pod <name>`: attach to a live Pod by name if
/// possible; otherwise launch `insomnia-pod --pod <name>` so the pod process
/// resumes from name-keyed state or creates a fresh same-name Pod.
PodName {
pod_name: String,
socket_override: Option<PathBuf>,
},
/// `tui -r` / `tui --resume`: open the Pod picker, then attach to the
/// `insomnia -r` / `insomnia --resume`: open the Pod picker, then attach to the
/// selected live Pod or restore the selected stopped Pod by name.
Resume,
/// `tui --session <UUID>`: skip the picker, go straight to the
/// `insomnia --session <UUID>`: skip the picker, go straight to the
/// resume name dialog with `id` baked in.
ResumeWithSession(SegmentId),
/// `tui --multi`: open the multi-Pod dashboard. This is intentionally
/// `insomnia --multi`: open the multi-Pod dashboard. This is intentionally
/// separate from `-r`/`--resume`, which keeps its single-Pod picker
/// meaning.
Multi,
@ -230,18 +232,18 @@ async fn main() -> ExitCode {
let mode = match parse_args() {
Ok(m) => m,
Err(e) => {
eprintln!("tui: {e}");
eprintln!("insomnia: {e}");
return ExitCode::FAILURE;
}
};
if let Err(e) = enable_raw_mode() {
eprintln!("tui: failed to enter raw mode: {e}");
eprintln!("insomnia: failed to enter raw mode: {e}");
return ExitCode::FAILURE;
}
if let Err(e) = execute!(io::stdout(), EnableBracketedPaste) {
let _ = disable_raw_mode();
eprintln!("tui: {e}");
eprintln!("insomnia: {e}");
return ExitCode::FAILURE;
}
@ -278,7 +280,7 @@ async fn main() -> ExitCode {
// duplicate. Other errors (pod-name failures, terminal setup
// hiccups, etc.) need surfacing here.
if e.downcast_ref::<spawn::SpawnError>().is_none() {
eprintln!("tui: {e}");
eprintln!("insomnia: {e}");
}
ExitCode::FAILURE
}
@ -289,33 +291,95 @@ async fn run_pod_name(
pod_name: String,
socket_override: Option<PathBuf>,
) -> Result<(), Box<dyn std::error::Error>> {
let preferred_socket = resolve_socket(&pod_name, socket_override.clone());
if let Some((_socket_path, client)) =
connect_live_pod(&pod_name, preferred_socket, socket_override.is_none()).await
{
if let Some(client) = try_connect_live_pod(&pod_name, socket_override.clone()).await {
let mut terminal = enter_fullscreen()?;
let mut app = App::new(pod_name);
app.connected = true;
return run_loop(&mut terminal, &mut app, client).await;
run_connected_pod(&mut terminal, pod_name, client).await?;
return Ok(());
}
let ready = match spawn::run_pod_name(pod_name).await? {
SpawnOutcome::Ready(r) => r,
SpawnOutcome::Cancelled => return Ok(()),
};
let mut terminal = enter_fullscreen()?;
terminal.clear()?;
let result = run_ready_pod(&mut terminal, ready).await;
let _ = leave_fullscreen(&mut terminal);
result
}
async fn run_connected_pod(
terminal: &mut FullscreenTerminal,
pod_name: String,
client: PodClient,
) -> Result<(), Box<dyn std::error::Error>> {
let mut app = App::new(pod_name);
app.connected = true;
run_loop(terminal, &mut app, client).await
}
async fn run_pod_name_nested(
terminal: &mut FullscreenTerminal,
request: multi_pod::OpenPodRequest,
) -> Result<(), Box<dyn std::error::Error>> {
let multi_pod::OpenPodRequest {
pod_name,
socket_override,
} = request;
if let Some(client) = try_connect_live_pod(&pod_name, socket_override).await {
return run_connected_pod(terminal, pod_name, client).await;
}
let ready = spawn_pod_name_from_fullscreen(terminal, &pod_name).await?;
run_ready_pod(terminal, ready).await
}
async fn spawn_pod_name_from_fullscreen(
terminal: &mut FullscreenTerminal,
pod_name: &str,
) -> Result<SpawnReady, Box<dyn std::error::Error>> {
leave_fullscreen(terminal)?;
let outcome = spawn::run_pod_name(pod_name.to_string()).await;
enter_fullscreen_existing(terminal)?;
terminal.clear()?;
match outcome? {
SpawnOutcome::Ready(ready) => Ok(ready),
SpawnOutcome::Cancelled => Err(Box::new(NestedOpenCancelled)),
}
}
async fn try_connect_live_pod(
pod_name: &str,
socket_override: Option<PathBuf>,
) -> Option<PodClient> {
let preferred_socket = resolve_socket(pod_name, socket_override.clone());
connect_live_pod(pod_name, preferred_socket, socket_override.is_none())
.await
.map(|(_, client)| client)
}
#[derive(Debug)]
struct NestedOpenCancelled;
impl std::fmt::Display for NestedOpenCancelled {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Pod open was cancelled")
}
}
impl std::error::Error for NestedOpenCancelled {}
async fn run_ready_pod(
terminal: &mut FullscreenTerminal,
ready: SpawnReady,
) -> Result<(), Box<dyn std::error::Error>> {
let SpawnReady {
pod_name,
socket_path,
} = ready;
let mut terminal = enter_fullscreen()?;
let result = run(&mut terminal, pod_name, &socket_path).await;
let _ = execute!(
terminal.backend_mut(),
DisableMouseCapture,
LeaveAlternateScreen
);
result
run(terminal, pod_name, &socket_path).await
}
async fn connect_live_pod(
@ -354,24 +418,37 @@ async fn run_resume() -> Result<(), Box<dyn std::error::Error>> {
}
async fn run_multi() -> Result<(), Box<dyn std::error::Error>> {
let mut app = multi_pod::load_app().await?;
let mut terminal = enter_fullscreen()?;
let outcome = multi_pod::run(&mut terminal).await;
let _ = execute!(
terminal.backend_mut(),
DisableMouseCapture,
LeaveAlternateScreen
);
match outcome? {
multi_pod::MultiPodOutcome::Quit => Ok(()),
multi_pod::MultiPodOutcome::Open {
pod_name,
socket_override,
} => run_pod_name(pod_name, socket_override).await,
loop {
match multi_pod::run(&mut terminal, &mut app).await? {
multi_pod::MultiPodOutcome::Quit => {
let _ = leave_fullscreen(&mut terminal);
return Ok(());
}
multi_pod::MultiPodOutcome::Open(request) => {
let pod_name = request.pod_name.clone();
match run_pod_name_nested(&mut terminal, request).await {
Ok(()) => app.finish_open(&pod_name, Ok(())),
Err(error) if is_recoverable_multi_open_error(error.as_ref()) => {
app.finish_open(&pod_name, Err(error.as_ref()));
}
Err(error) => {
let _ = leave_fullscreen(&mut terminal);
return Err(error);
}
}
app.reload().await?;
}
}
}
}
fn is_recoverable_multi_open_error(error: &(dyn std::error::Error + 'static)) -> bool {
error.is::<spawn::SpawnError>() || error.is::<NestedOpenCancelled>()
}
async fn run_spawn(resume_from: Option<SegmentId>) -> Result<(), Box<dyn std::error::Error>> {
let ready = match spawn::run(resume_from).await? {
SpawnOutcome::Ready(r) => r,
@ -396,16 +473,34 @@ async fn run_spawn(resume_from: Option<SegmentId>) -> Result<(), Box<dyn std::er
result
}
fn enter_fullscreen() -> Result<Terminal<CrosstermBackend<io::Stdout>>, Box<dyn std::error::Error>>
{
fn enter_fullscreen() -> Result<FullscreenTerminal, Box<dyn std::error::Error>> {
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
Ok(Terminal::new(backend)?)
}
fn enter_fullscreen_existing(
terminal: &mut FullscreenTerminal,
) -> Result<(), Box<dyn std::error::Error>> {
execute!(
terminal.backend_mut(),
EnterAlternateScreen,
EnableMouseCapture
)?;
Ok(())
}
fn leave_fullscreen(terminal: &mut FullscreenTerminal) -> io::Result<()> {
execute!(
terminal.backend_mut(),
DisableMouseCapture,
LeaveAlternateScreen
)
}
async fn run(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
terminal: &mut FullscreenTerminal,
pod_name: String,
socket_path: &std::path::Path,
) -> Result<(), Box<dyn std::error::Error>> {
@ -438,7 +533,7 @@ const POD_EVENT_DRAIN_LIMIT: usize = 32;
struct TerminalEventReader {
stop: Arc<AtomicBool>,
_thread: thread::JoinHandle<()>,
thread: Option<thread::JoinHandle<()>>,
}
impl TerminalEventReader {
@ -453,7 +548,7 @@ impl TerminalEventReader {
Ok((
Self {
stop,
_thread: thread,
thread: Some(thread),
},
rx,
))
@ -463,6 +558,9 @@ impl TerminalEventReader {
impl Drop for TerminalEventReader {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
}
}
@ -703,6 +801,9 @@ 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())
@ -782,6 +883,25 @@ 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.
@ -894,7 +1014,7 @@ fn handle_command_key(app: &mut App, key: KeyEvent) -> Option<Method> {
app.exit_command_mode();
None
}
KeyCode::Enter => app.submit_command(),
KeyCode::Enter => app.submit_command_with_completion(),
KeyCode::Backspace => {
if app.command_text().is_empty() {
app.exit_command_mode();
@ -916,11 +1036,19 @@ fn handle_command_key(app: &mut App, key: KeyEvent) -> Option<Method> {
None
}
KeyCode::Up => {
app.move_cursor_up();
if app.command_completion_active() {
app.move_command_completion_up();
} else {
app.move_cursor_up();
}
None
}
KeyCode::Down => {
app.move_cursor_down();
if app.command_completion_active() {
app.move_command_completion_down();
} else {
app.move_cursor_down();
}
None
}
KeyCode::Home => {
@ -931,7 +1059,10 @@ fn handle_command_key(app: &mut App, key: KeyEvent) -> Option<Method> {
app.move_cursor_end();
None
}
KeyCode::Tab => None,
KeyCode::Tab => {
app.apply_command_completion();
None
}
KeyCode::Char(c) => {
if key
.modifiers
@ -970,6 +1101,7 @@ 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() {
@ -1433,6 +1565,191 @@ 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());
@ -1460,6 +1777,204 @@ 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

@ -40,7 +40,7 @@ impl std::fmt::Display for MultiPodError {
Self::Store(e) => write!(f, "session store error: {e}"),
Self::NoPods => write!(
f,
"no pods found — start a fresh pod with `tui` or restore one with `tui -r`"
"no pods found — start a fresh pod with `insomnia` or restore one with `insomnia -r`"
),
}
}
@ -62,37 +62,41 @@ impl From<session_store::StoreError> for MultiPodError {
pub(crate) enum MultiPodOutcome {
Quit,
Open {
pod_name: String,
socket_override: Option<PathBuf>,
},
Open(OpenPodRequest),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct OpenPodRequest {
pub(crate) pod_name: String,
pub(crate) socket_override: Option<PathBuf>,
}
pub(crate) async fn load_app() -> Result<MultiPodApp, MultiPodError> {
MultiPodApp::load(None).await
}
pub(crate) async fn run(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
app: &mut MultiPodApp,
) -> Result<MultiPodOutcome, MultiPodError> {
let mut app = MultiPodApp::load(None).await?;
if app.list.entries.is_empty() {
return Err(MultiPodError::NoPods);
}
loop {
terminal.draw(|f| draw(f, &mut app))?;
terminal.draw(|f| draw(f, app))?;
match read()? {
TermEvent::Key(key) => match app.handle_key(key) {
MultiPodAction::None => {}
MultiPodAction::Quit => return Ok(MultiPodOutcome::Quit),
MultiPodAction::Open => {
if let Some(entry) = app.list.selected_entry() {
return Ok(MultiPodOutcome::Open {
pod_name: entry.name.clone(),
socket_override: entry.attach_socket_path().map(PathBuf::from),
});
if let Some(request) = app.prepare_open() {
return Ok(MultiPodOutcome::Open(request));
}
}
MultiPodAction::Refresh => app.reload().await?,
MultiPodAction::Send(request) => {
terminal.draw(|f| draw(f, &mut app))?;
terminal.draw(|f| draw(f, app))?;
let result = send_run_and_confirm(&request.socket_path, request.segments).await;
app.finish_send(result);
let _ = app.reload().await;
@ -147,7 +151,7 @@ impl MultiPodApp {
Ok(app)
}
async fn reload(&mut self) -> Result<(), MultiPodError> {
pub(crate) async fn reload(&mut self) -> Result<(), MultiPodError> {
self.list = load_pod_list(self.list.selected_name.clone()).await?;
self.ensure_selection_visible();
Ok(())
@ -211,6 +215,44 @@ impl MultiPodApp {
}
}
pub(crate) fn prepare_open(&mut self) -> Option<OpenPodRequest> {
let entry = match self.list.selected_entry() {
Some(entry) => entry,
None => {
self.notice = Some("No Pod is selected.".to_string());
return None;
}
};
if !entry.actions.can_open {
self.notice = Some("Selected Pod cannot be opened from this view.".to_string());
return None;
}
self.notice = Some(format!("Opening {}", entry.name));
Some(OpenPodRequest {
pod_name: entry.name.clone(),
socket_override: entry.attach_socket_path().map(PathBuf::from),
})
}
pub(crate) fn finish_open(
&mut self,
pod_name: &str,
result: Result<(), &dyn std::fmt::Display>,
) {
match result {
Ok(()) => {
self.notice = Some(format!("Returned from {pod_name}."));
}
Err(error) => {
self.notice = Some(format!("Open failed for {pod_name}: {error}"));
}
}
}
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,
@ -287,6 +329,7 @@ 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)
@ -654,8 +697,10 @@ 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 input_render = app.input.render(input_content_width);
let mut 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);
@ -835,7 +880,8 @@ 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 prefix = if i == 0 { "> " } else { " " };
let absolute_row = render.viewport_start_row as usize + i;
let prefix = if absolute_row == 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));
@ -1114,6 +1160,123 @@ mod tests {
assert!(app.notice.as_deref().unwrap().contains("Delivered"));
}
#[test]
fn multi_open_request_keeps_dashboard_state_for_nested_single_pod() {
let mut app = test_app(vec![live_info("alpha", PodStatus::Idle)]);
app.input.insert_str("draft survives 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!(app.list.selected_entry().unwrap().name, "alpha");
assert_eq!(input_text(&app), "draft survives open");
assert!(app.notice.as_deref().unwrap().contains("Opening alpha"));
}
#[test]
fn multi_open_failure_keeps_composer_and_sets_notice() {
let mut app = test_app(vec![live_info("alpha", PodStatus::Idle)]);
app.input.insert_str("keep this draft");
let before = input_text(&app);
let error = io::Error::other("boom");
app.finish_open("alpha", Err(&error));
assert_eq!(input_text(&app), before);
assert_eq!(app.list.selected_entry().unwrap().name, "alpha");
assert!(
app.notice
.as_deref()
.unwrap()
.contains("Open failed for alpha")
);
}
#[test]
fn multi_open_disabled_target_stays_in_dashboard() {
let mut live = live_info("unreachable", PodStatus::Idle);
live.reachable = false;
live.status = None;
let mut app = test_app(vec![live]);
assert!(app.prepare_open().is_none());
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,
@ -1153,6 +1316,13 @@ 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,
@ -1206,4 +1376,8 @@ 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

@ -2,7 +2,7 @@
//!
//! Reads live Pod allocations from the runtime registry and stopped Pod state
//! from the session store's name-keyed metadata. Picking a live row attaches to
//! its socket; picking a stopped row restores via `pod --pod <name>`.
//! its socket; picking a stopped row restores via `insomnia-pod --pod <name>`.
use std::io;
use std::path::PathBuf;
@ -41,7 +41,7 @@ impl std::fmt::Display for PickerError {
Self::Store(e) => write!(f, "session store error: {e}"),
Self::NoPods => write!(
f,
"no pods found — start a fresh pod with `tui` and try again"
"no pods found — start a fresh pod with `insomnia` and try again"
),
}
}
@ -64,7 +64,7 @@ impl From<session_store::StoreError> for PickerError {
pub enum PickerOutcome {
/// User picked a Pod. `socket_override` is set for live rows when the
/// runtime registry knows the exact socket path; stopped rows leave it
/// empty so the caller restores with `pod --pod <name>`.
/// empty so the caller restores with `insomnia-pod --pod <name>`.
Picked {
pod_name: String,
socket_override: Option<PathBuf>,

View File

@ -1,9 +1,9 @@
//! Inline-viewport "spawn Pod and attach" UX.
//!
//! Rendered at the user's current cursor position when `tui` is invoked
//! Rendered at the user's current cursor position when `insomnia` is invoked
//! with no positional argument. Walks the cwd for a `.insomnia/manifest.toml`
//! to seed defaults, prompts for the Pod's name, and on confirmation
//! launches the `pod` binary as an independent process with a freshly built
//! launches the `insomnia-pod` binary as an independent process with a freshly built
//! overlay (name + cwd scope when no project manifest exists). Once
//! the process reports its socket via the `INSOMNIA-READY` stderr line,
//! the dialog hands control back so main can switch the terminal to
@ -90,7 +90,7 @@ type InlineTerminal = Terminal<CrosstermBackend<io::Stdout>>;
/// Source session for a resume run. `None` = fresh spawn (current
/// behaviour); `Some(id)` swaps the dialog into "Resume Pod" mode and
/// passes `--session <id>` to the spawned `pod` child.
/// passes `--session <id>` to the spawned `insomnia-pod` child.
pub async fn run(resume_from: Option<SegmentId>) -> Result<SpawnOutcome, SpawnError> {
let defaults = load_spawn_defaults()?;
@ -170,7 +170,7 @@ pub async fn run(resume_from: Option<SegmentId>) -> Result<SpawnOutcome, SpawnEr
}
}
/// Launch `pod --pod <name>` without opening the name dialog. The child Pod
/// Launch `insomnia-pod --pod <name>` without opening the name dialog. The child Pod
/// resolves persisted Pod metadata if present, or creates a fresh same-name Pod
/// with the usual TUI cwd-scope fallback.
pub async fn run_pod_name(pod_name: String) -> Result<SpawnOutcome, SpawnError> {

View File

@ -65,12 +65,19 @@ 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 input_render = if app.is_command_mode() {
let mut 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.
@ -284,11 +291,29 @@ 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);
for candidate in visible_suggestions.iter().take(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()
};
lines.push(Line::from(vec![
Span::styled(candidate.name.to_owned(), command_style),
Span::styled("", description_style),
Span::styled(candidate.description.to_owned(), description_style),
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),
),
]));
}
@ -394,6 +419,11 @@ 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
@ -422,6 +452,99 @@ 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.
@ -1284,7 +1407,12 @@ 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 prefix = if i == 0 { prompt } else { continuation };
let absolute_row = render.viewport_start_row as usize + i;
let prefix = if absolute_row == 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

@ -1,11 +1,11 @@
{ pkgs }:
let
# Dev-only wrapper. tui の spawn 経路は `pod` バイナリを直に exec し、
# Dev-only wrapper. tui の spawn 経路は `insomnia-pod` バイナリを直に exec し、
# stderr の `INSOMNIA-READY` 行で握手するので、cargo の進捗や rustc の
# warning が混ざると tail に余計な行が積もり本当のエラーが押し出される。
# ここで一度ビルドを切り離し、成功時はビルド出力を一切捨てて素のバイナリ
# を exec、失敗時のみ build log を stderr に流して exit する。
pod-dev = pkgs.writeShellScriptBin "pod" ''
pod-dev = pkgs.writeShellScriptBin "insomnia-pod" ''
set -u
buildlog=$(mktemp)
trap 'rm -f "$buildlog"' EXIT
@ -15,7 +15,7 @@ let
fi
manifest=$(cargo locate-project --workspace --message-format plain 2>/dev/null)
target_dir=''${CARGO_TARGET_DIR:-$(dirname "$manifest")/target}
exec "$target_dir/debug/pod" "$@"
exec "$target_dir/debug/insomnia-pod" "$@"
'';
in
pkgs.mkShell {

View File

@ -10,12 +10,12 @@ From the repository root:
nix build .#
```
The default package is implemented by `package.nix` and builds the Cargo workspace binaries `pod` and `tui`. The derivation uses the checked-in `Cargo.lock`, so Cargo dependencies are fetched by the normal Nix Rust packaging path instead of by network access during the build.
The default package is implemented by `package.nix` and builds the Cargo packages `pod` and `tui` as installed binaries `insomnia-pod` and `insomnia`. The derivation uses the checked-in `Cargo.lock`, so Cargo dependencies are fetched by the normal Nix Rust packaging path instead of by network access during the build.
The package output contains:
- `bin/pod` — Pod CLI / runtime process.
- `bin/tui` — terminal UI.
- `bin/insomnia-pod` — Pod CLI / runtime process.
- `bin/insomnia` — terminal UI.
- `share/insomnia/resources/` — bundled runtime resources, including `resources/prompts/`.
- `share/doc/insomnia/nix.md` — this document.
@ -24,15 +24,15 @@ The package output contains:
After `nix build`:
```sh
./result/bin/pod --help
./result/bin/tui
./result/bin/insomnia-pod --help
./result/bin/insomnia
```
With flakes:
```sh
nix run .#tui
nix run .#pod -- --help
nix run .#insomnia
nix run .#insomnia-pod -- --help
```
`nix run .#` defaults to the TUI.
@ -53,8 +53,8 @@ The Nix package does not put user configuration, sessions, sockets, or other mut
The package derivation has a credential-free install check that verifies:
- `pod --help` starts successfully.
- `tui` is installed and reaches argument parsing.
- `insomnia-pod --help` starts successfully.
- `insomnia` is installed and reaches argument parsing.
- bundled prompt resources and this Nix usage document are present in the output.
For full validation before handing changes to review, run:

View File

@ -172,7 +172,7 @@ unique であれば workspace を指定しなくて済む。
## Daemon-less リモート Pod 生成SSH-only モデル)
リモートホスト上の Pod 生成は **daemon 無しで SSH だけで成立する**
remote 側に必要なのは `pod` バイナリと SSH アクセスのみ。
remote 側に必要なのは `insomnia-pod` バイナリと SSH アクセスのみ。
### 前提
@ -182,7 +182,7 @@ remote 側に必要なのは `pod` バイナリと SSH アクセスのみ。
- insomnia が転送するのは**セッション(会話履歴)と manifest overlay**
だけ。コードベースの同期は外部に委ねる
- コンテナ内で動かすか bare metal で動かすかも insomnia は問わない。
`pod` バイナリが動くホストの fs 上で活動する主体がある、
`insomnia-pod` バイナリが動くホストの fs 上で活動する主体がある、
それだけが前提
### フロー
@ -193,7 +193,7 @@ host_a (spawner) host_b (remote)
├── ssh: session データを転送 ────────→ ファイル書き込み
├── ssh: overlay TOML を転送 ─────────→ ファイル書き込み
├── ssh: `pod --overlay ... &` ───────→ Pod プロセス起動、socket 作成
├── ssh: `insomnia-pod --overlay ... &` ───────→ Pod プロセス起動、socket 作成
├── ssh -L: socket を tunnel ─────────→ Pod B の unix socket
└── localhost:tunnel に接続 ──────────→ Method::Run / Event stream
@ -209,7 +209,7 @@ tar cz session/ | ssh insomnia@host-b "tar xz -C ~/workspaces/task-123/store"
echo "$OVERLAY" | ssh insomnia@host-b "cat > ~/workspaces/task-123/overlay.toml"
# 2. Pod を起動detach
ssh insomnia@host-b "pod --store ~/workspaces/task-123/store \
ssh insomnia@host-b "insomnia-pod --store ~/workspaces/task-123/store \
--overlay ~/workspaces/task-123/overlay.toml &"
# 3. socket を tunnel で引っ張る

View File

@ -56,7 +56,7 @@ manifest 中のパス(`model.auth.file` / `scope.*.target` /
Pod の作業ディレクトリは manifest に含まれない。プロセス起動時の
`std::env::current_dir()` がそのまま Pod の pwd となるため、別の作業
ディレクトリで Pod を走らせたい場合は `cd` してから `pod` を起動する
ディレクトリで Pod を走らせたい場合は `cd` してから `insomnia-pod` を起動する
(または `SpawnPod` が子に対して行っているように、親プロセス側で
`Command::current_dir` を明示する)。
@ -297,12 +297,12 @@ import-map 形式のプレフィックスで指定する:
---
## `pod` CLI
## `insomnia-pod` CLI
`pod` は通常、builtin default → user manifest → project manifest → overlay の cascade で manifest を解決して起動する。
`insomnia-pod` は通常、builtin default → user manifest → project manifest → overlay の cascade で manifest を解決して起動する。
```
pod [--project <path>] [--overlay <toml>] [-s/--store <path>] [--session <uuid>]
insomnia-pod [--project <path>] [--overlay <toml>] [-s/--store <path>] [--session <uuid>]
```
| フラグ | 説明 |
@ -323,15 +323,15 @@ user manifest は CLI フラグではなく、以下の規則で解決する。
単一ファイルだけで起動したい場合は cascade を使わず、`--manifest` を指定する。
```
pod --manifest <path> [-s/--store <path>] [--session <uuid>]
insomnia-pod --manifest <path> [-s/--store <path>] [--session <uuid>]
```
`--manifest` は指定 TOML 1 枚だけを `PodManifest::from_toml` で読み、user / project / overlay layer は一切読まない。したがって `--project`、`--overlay`、非空の `INSOMNIA_USER_MANIFEST` とは併用不可。
spawn 子 Pod 用の内部フラグとして `--adopt``--callback <path>` がある。これらは `SpawnPod` が scope allocation と親 callback socket を引き継がせるために使うもので、通常の手動起動では使わない。
Pod の作業ディレクトリは `pod` 起動時の cwd が直接使われる。別ディレクトリで
動かしたい場合は `cd <path> && pod ...` のように外側で `cd` してから起動する。
Pod の作業ディレクトリは `insomnia-pod` 起動時の cwd が直接使われる。別ディレクトリで
動かしたい場合は `cd <path> && insomnia-pod ...` のように外側で `cd` してから起動する。
引数無しで起動すると、cwd + `manifest::paths` の自動解決だけで動く最小構成になる
overlay 無し、プロジェクトに `.insomnia/manifest.toml` があればそれを使う)。

View File

@ -111,7 +111,7 @@ Running 中に割り込みたい場合、ほとんどのケースで `Ctrl-C`
`Ctrl-X` は Running 中だけ Cancel、Idle / Paused では Shutdown。`Ctrl-C` は Running 中だけ Pod に `Method::Pause` を送り、それ以外では Pod は落とさず TUI プロセスだけ抜ける。`Ctrl-D` は常に Pod へ制御メソッドを送らず TUI プロセスだけ抜ける。
TUI のダイアログから Pod を起動する経路では、起動した Pod は TUI の子プロセスとして管理・終了されず、独立したプロセスとして残る。TUI 終了後は `tui <pod-name>` で再接続できる。
TUI のダイアログから Pod を起動する経路では、起動した Pod は TUI の子プロセスとして管理・終了されず、独立したプロセスとして残る。TUI 終了後は `insomnia <pod-name>` で再接続できる。
## 履歴メモ

View File

@ -1,217 +0,0 @@
# 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は対話的にプロジェクトを統括するツールとしての価値を提供する。

View File

@ -1,25 +0,0 @@
# 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

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

View File

@ -27,9 +27,9 @@
packages.default = insomnia;
packages.insomnia = insomnia;
apps.default = mkApp "tui" "Run the INSOMNIA terminal UI";
apps.tui = mkApp "tui" "Run the INSOMNIA terminal UI";
apps.pod = mkApp "pod" "Run the INSOMNIA Pod CLI";
apps.default = mkApp "insomnia" "Run the INSOMNIA terminal UI";
apps.insomnia = mkApp "insomnia" "Run the INSOMNIA terminal UI";
apps.insomnia-pod = mkApp "insomnia-pod" "Run the INSOMNIA Pod CLI";
checks.default = insomnia;

View File

@ -40,7 +40,30 @@ rustPlatform.buildRustPackage rec {
filter = sourceFilter;
};
cargoLock.lockFile = ./Cargo.lock;
cargoHash = "sha256-8ZT5moKFxj/5vbp5rsUG7UkPLY1fvQKhYTyjRWQ58xk=";
depsExtraArgs = {
# nixpkgs 25.11's fetchCargoVendor still uses crates.io's API
# download endpoint in this environment, which returns 403 while the
# immutable static CDN endpoint works. Keep this local package build on
# static.crates.io until the upstream fetcher is fixed in our nixpkgs pin.
buildPhase = ''
runHook preBuild
if [ -n "''${cargoRoot-}" ]; then
cd "$cargoRoot"
fi
vendor_util="$(command -v fetch-cargo-vendor-util-v2 || command -v fetch-cargo-vendor-util)"
cp "$vendor_util" ./fetch-cargo-vendor-util-static
substituteInPlace ./fetch-cargo-vendor-util-static \
--replace-fail 'https://crates.io/api/v1/crates/{pkg["name"]}/{pkg["version"]}/download' \
'https://static.crates.io/crates/{pkg["name"]}/{pkg["version"]}/download'
./fetch-cargo-vendor-util-static create-vendor-staging ./Cargo.lock "$out"
runHook postBuild
'';
};
strictDeps = true;
@ -80,13 +103,13 @@ rustPlatform.buildRustPackage rec {
installCheckPhase = ''
runHook preInstallCheck
"$out/bin/pod" --help >/dev/null
test -x "$out/bin/tui"
if "$out/bin/tui" --session not-a-uuid 2>tui.err; then
echo "tui unexpectedly accepted an invalid --session value" >&2
"$out/bin/insomnia-pod" --help >/dev/null
test -x "$out/bin/insomnia"
if "$out/bin/insomnia" --session not-a-uuid 2>insomnia.err; then
echo "insomnia unexpectedly accepted an invalid --session value" >&2
exit 1
fi
grep -q "invalid --session UUID" tui.err
grep -q "invalid --session UUID" insomnia.err
test -d "$out/share/insomnia/resources/prompts"
test -f "$out/share/doc/insomnia/nix.md"
@ -97,7 +120,7 @@ rustPlatform.buildRustPackage rec {
meta = {
description = "Agentic coding Pod runtime and terminal UI";
license = lib.licenses.mit;
mainProgram = "tui";
mainProgram = "insomnia";
platforms = lib.platforms.unix;
};
}

View File

@ -5,3 +5,10 @@ When searching, use grep/glob primitives rather than shell pipelines.
You can run multiple tools simultaneously by calling them within a single response.
It is recommended to run tools that handle asynchronous processing, such as queries and readings, in batches.
### Memory and knowledge
For past decisions, prior requests, durable preferences, project history, or why something was done, use targeted lookup instead of guessing from vague recollection.
Use `MemoryQuery` for durable memory records (summary, decisions, requests), `KnowledgeQuery` for project knowledge, `MemoryRead(kind=summary)` for the full memory summary, and `MemoryRead` on returned slugs when excerpts are insufficient.
Resident memory and knowledge are helpful context but may be stale; current user instructions, repository files, tickets, git history, and session logs are authoritative for exact current state.
Do not query memory every turn, and normally prefer read/query tools; use `MemoryWrite`, `MemoryEdit`, or `MemoryDelete` only when explicitly asked or in a memory maintenance worker.

View File

@ -0,0 +1,155 @@
---
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

@ -0,0 +1,155 @@
---
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

@ -0,0 +1,170 @@
<!-- 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

@ -2,12 +2,12 @@
id: 20260527-000005-memory-tool-guidance-prompt
slug: memory-tool-guidance-prompt
title: プロンプト: memory / knowledge tool 利用タイミングのガイダンス
status: open
status: closed
kind: task
priority: P2
labels: [migrated]
created_at: 2026-05-27T00:00:05Z
updated_at: 2026-05-27T00:00:05Z
updated_at: 2026-05-28T23:59:06Z
assignee: null
legacy_ticket: tickets/memory-tool-guidance-prompt.md
---

View File

@ -0,0 +1,87 @@
---
id: 20260527-000005-memory-tool-guidance-prompt
slug: memory-tool-guidance-prompt
title: プロンプト: memory / knowledge tool 利用タイミングのガイダンス
status: closed
kind: task
priority: P2
labels: [migrated]
created_at: 2026-05-27T00:00:05Z
updated_at: 2026-05-28T23:59:06Z
assignee: null
legacy_ticket: tickets/memory-tool-guidance-prompt.md
---
## Migration reference
- legacy_ticket: tickets/memory-tool-guidance-prompt.md
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
# プロンプト: memory / knowledge tool 利用タイミングのガイダンス
## 背景
通常 Pod には `MemoryQuery` / `MemoryRead` / `KnowledgeQuery` / `MemoryWrite` 等の memory / knowledge tools が提供されているが、現状の通常 system prompt はそれらを「いつ使うべきか」をほとんど説明していない。
現在の `resources/prompts/common/tool-usage.md` は、既知パスなら Read、検索なら Grep/Glob、並列可能ならまとめる、という汎用 tool 方針に留まる。memory / knowledge tools の description には操作方法はあるが、モデルが自発的に memory lookup すべき状況は明示されていない。
このため、過去の決定・ユーザー嗜好・以前の経緯を問われても、モデルが `MemoryQuery` / `MemoryRead` を自発的に使わない可能性が高い。`summary.md` resident injection により短い durable context は常時見えるようになるが、詳細な過去判断や request を探すには query guidance が必要である。
## 方針
通常 Pod の system prompt に、memory / knowledge tools の利用タイミングを短く追加する。
目的は「必要な時に過去情報を探す」ことであり、毎 turn memory query を強制することではない。memory / knowledge は helpful context だが stale になり得るため、現在の user instruction / files / tickets / git state / session log を上書きする権威として扱わせない。
## 推奨する追加文言
`resources/prompts/common/tool-usage.md` に新しい小節を足すか、`resources/prompts/common/memory.md` を作って `default.md` から include する。
例:
```md
## Memory and knowledge
Use memory and knowledge tools when the user asks about past decisions, prior requests, durable preferences, project history, or why something was done. Do not guess from vague recollection when a targeted memory lookup would answer the question.
- Use `MemoryQuery` for durable memory records: summary, decisions, and requests.
- Use `KnowledgeQuery` for project knowledge records.
- Use `MemoryRead(kind=summary)` when you need the full workspace memory summary.
- Use `MemoryRead` on returned slugs when query excerpts are insufficient.
Resident memory and knowledge are helpful context but may be stale. Current user instructions, repository files, tickets, git history, and session logs are more authoritative for exact current state.
Do not query memory on every turn. Prefer it when past context, user preferences, or prior rationale materially affects the answer or implementation.
```
文言は実装時に自然に調整してよいが、以下の意味は維持する。
- 過去判断 / 過去依頼 / ユーザー嗜好 / project history / why 系では memory lookup を促す。
- `MemoryQuery`, `KnowledgeQuery`, `MemoryRead(kind=summary)`, slug read の役割を明示する。
- resident context は stale になり得ると明示する。
- current user instruction / files / tickets / git / session logs の方が exact current state では強いと明示する。
- 毎 turn query しないと明示する。
## 要件
- 通常 Pod の default prompt に memory / knowledge tool 利用タイミングの guidance が入る。
- internal prompts (`memory_extract_system`, `memory_consolidation_system`, `compact_system`) の挙動を変えない。
- guidance は短く、通常 turn の token overhead を過度に増やさない。
- guidance は memory / knowledge を current authority より上に置かない。
- guidance は毎 turn memory query を促さない。
- `MemoryWrite` / `MemoryEdit` / `MemoryDelete` の自発的利用を安易に促さない。
- 通常作業では read/query を促し、write/edit/delete は明示的な依頼または memory maintenance worker に寄せる。
## 完了条件
- `resources/prompts/default.md` から memory guidance が render される。
- prompt render / catalog 関連 test があれば更新されている。
- internal worker prompt には不要な memory guidance が混ざらない。
- `cargo fmt --check` と関連 test が通る。
## 範囲外
- `summary.md` resident injection の実装。これは `memory-summary-resident-injection.md` で扱う。
- memory tool descriptions の大幅変更。
- memory usage metrics の設計変更。
- global memory / project local memory の store 分離。

View File

@ -0,0 +1,102 @@
<!-- event: migration author: tickets.sh-migration at: 2026-05-27T00:00:05Z -->
## Migrated
Migrated from tickets/memory-tool-guidance-prompt.md. No legacy review file was present at migration time.
---
<!-- event: close author: hare at: 2026-05-28T23:59:06Z status: closed -->
## Closed
---
id: 20260527-000005-memory-tool-guidance-prompt
slug: memory-tool-guidance-prompt
title: プロンプト: memory / knowledge tool 利用タイミングのガイダンス
status: closed
kind: task
priority: P2
labels: [migrated]
created_at: 2026-05-27T00:00:05Z
updated_at: 2026-05-28T23:59:06Z
assignee: null
legacy_ticket: tickets/memory-tool-guidance-prompt.md
---
## Migration reference
- legacy_ticket: tickets/memory-tool-guidance-prompt.md
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
# プロンプト: memory / knowledge tool 利用タイミングのガイダンス
## 背景
通常 Pod には `MemoryQuery` / `MemoryRead` / `KnowledgeQuery` / `MemoryWrite` 等の memory / knowledge tools が提供されているが、現状の通常 system prompt はそれらを「いつ使うべきか」をほとんど説明していない。
現在の `resources/prompts/common/tool-usage.md` は、既知パスなら Read、検索なら Grep/Glob、並列可能ならまとめる、という汎用 tool 方針に留まる。memory / knowledge tools の description には操作方法はあるが、モデルが自発的に memory lookup すべき状況は明示されていない。
このため、過去の決定・ユーザー嗜好・以前の経緯を問われても、モデルが `MemoryQuery` / `MemoryRead` を自発的に使わない可能性が高い。`summary.md` resident injection により短い durable context は常時見えるようになるが、詳細な過去判断や request を探すには query guidance が必要である。
## 方針
通常 Pod の system prompt に、memory / knowledge tools の利用タイミングを短く追加する。
目的は「必要な時に過去情報を探す」ことであり、毎 turn memory query を強制することではない。memory / knowledge は helpful context だが stale になり得るため、現在の user instruction / files / tickets / git state / session log を上書きする権威として扱わせない。
## 推奨する追加文言
`resources/prompts/common/tool-usage.md` に新しい小節を足すか、`resources/prompts/common/memory.md` を作って `default.md` から include する。
例:
```md
## Memory and knowledge
Use memory and knowledge tools when the user asks about past decisions, prior requests, durable preferences, project history, or why something was done. Do not guess from vague recollection when a targeted memory lookup would answer the question.
- Use `MemoryQuery` for durable memory records: summary, decisions, and requests.
- Use `KnowledgeQuery` for project knowledge records.
- Use `MemoryRead(kind=summary)` when you need the full workspace memory summary.
- Use `MemoryRead` on returned slugs when query excerpts are insufficient.
Resident memory and knowledge are helpful context but may be stale. Current user instructions, repository files, tickets, git history, and session logs are more authoritative for exact current state.
Do not query memory on every turn. Prefer it when past context, user preferences, or prior rationale materially affects the answer or implementation.
```
文言は実装時に自然に調整してよいが、以下の意味は維持する。
- 過去判断 / 過去依頼 / ユーザー嗜好 / project history / why 系では memory lookup を促す。
- `MemoryQuery`, `KnowledgeQuery`, `MemoryRead(kind=summary)`, slug read の役割を明示する。
- resident context は stale になり得ると明示する。
- current user instruction / files / tickets / git / session logs の方が exact current state では強いと明示する。
- 毎 turn query しないと明示する。
## 要件
- 通常 Pod の default prompt に memory / knowledge tool 利用タイミングの guidance が入る。
- internal prompts (`memory_extract_system`, `memory_consolidation_system`, `compact_system`) の挙動を変えない。
- guidance は短く、通常 turn の token overhead を過度に増やさない。
- guidance は memory / knowledge を current authority より上に置かない。
- guidance は毎 turn memory query を促さない。
- `MemoryWrite` / `MemoryEdit` / `MemoryDelete` の自発的利用を安易に促さない。
- 通常作業では read/query を促し、write/edit/delete は明示的な依頼または memory maintenance worker に寄せる。
## 完了条件
- `resources/prompts/default.md` から memory guidance が render される。
- prompt render / catalog 関連 test があれば更新されている。
- internal worker prompt には不要な memory guidance が混ざらない。
- `cargo fmt --check` と関連 test が通る。
## 範囲外
- `summary.md` resident injection の実装。これは `memory-summary-resident-injection.md` で扱う。
- memory tool descriptions の大幅変更。
- memory usage metrics の設計変更。
- global memory / project local memory の store 分離。
---

View File

@ -0,0 +1,80 @@
---
id: 20260528-233524-multi-pod-open-return
slug: multi-pod-open-return
title: Return to multi-Pod view after opening a Pod
status: closed
kind: task
priority: P2
labels: [tui, pod, ux]
created_at: 2026-05-28T23:35:24Z
updated_at: 2026-05-28T23:57:49Z
assignee: null
legacy_ticket: null
---
## Background
`tui --multi` can open the selected Pod with `o`. The current implementation treats this as leaving the multi-Pod dashboard and tail-calling the normal single-Pod TUI. Once the single-Pod screen is detached with `Ctrl+D` / `Ctrl+C`, the process exits instead of returning to the multi-Pod view.
For now, no special "back mode" or dedicated back key is needed. The desired behavior is simpler: when the user opens a Pod from the multi-Pod dashboard, the normal single-Pod attach screen runs as a nested attach session. When that session exits normally by detach/quit (`Ctrl+D`, `Ctrl+C`, or equivalent normal exit), control returns to the multi-Pod dashboard.
This should be implemented by abstracting the TUI launch/control flow, not by adding protocol features or making the single-Pod App deeply aware of multi-Pod mode.
## Requirements
- `tui --multi` remains the entrypoint for the multi-Pod dashboard.
- In `tui --multi`, pressing `o` on a selected openable Pod opens the normal single-Pod conversation screen.
- When that single-Pod screen exits normally, TUI returns to the multi-Pod dashboard instead of exiting the process.
- `Ctrl+D` / `Ctrl+C` detach/quit from the opened single-Pod screen should return to multi view.
- Normal single-Pod launches such as `tui <pod>` / `tui --pod <name>` should continue to exit the process on `Ctrl+D` / `Ctrl+C`.
- Avoid introducing a dedicated back key or back mode for this ticket.
- The caller loop determines whether a normal single-Pod exit returns to multi view or exits the process.
- Preserve multi-Pod dashboard state where practical.
- The selected Pod name should remain selected after returning, if still visible.
- The multi-Pod composer contents should be preserved across open/return.
- The Pod list should refresh after returning so status changes are visible.
- Keep terminal handling clean.
- Do not unnecessarily leave/re-enter alternate screen between multi view and the nested single-Pod screen if the existing terminal can be reused safely.
- If reusing the same `Terminal`, ensure cursor/mouse/raw-mode cleanup remains correct on final exit and on errors.
- Error handling should be explicit.
- If opening the selected Pod fails before the single-Pod screen starts, show a multi-view notice and keep the dashboard active.
- If the single-Pod session exits with a fatal error, return that error or show a clear diagnostic according to the existing TUI error behavior; do not silently swallow fatal failures.
- Existing `tui --multi` direct send behavior, section layout, and separator fix must continue to work.
## Suggested implementation direction
- Split the current single-Pod attach runner into a reusable function that accepts an existing `Terminal` and returns when the attached screen exits.
- Change `run_multi()` from one-shot `multi_pod::run(...).await -> Open -> run_pod_name(...)` into a controller loop:
```text
loop:
run multi dashboard with previous app state / selected Pod
Quit => exit process
Open(pod) => run single-Pod attach screen using the same terminal
on normal exit, refresh dashboard and continue loop
```
- Avoid adding a new protocol method. This is local TUI orchestration.
- Avoid making `App` carry a generic `BackToMulti` mode unless it is strictly necessary; prefer caller-owned control flow.
## Acceptance criteria
- From `tui --multi`, pressing `o` opens the selected Pod's normal conversation screen.
- Pressing `Ctrl+D` / `Ctrl+C` in that opened screen returns to the multi-Pod dashboard.
- Starting a single-Pod TUI directly still exits on `Ctrl+D` / `Ctrl+C`.
- Returning to multi view preserves multi composer contents and selected Pod name when possible.
- Returning to multi view refreshes Pod status/list.
- Opening failure from multi view leaves the user in multi view with a visible notice.
- Existing multi-Pod tests still pass.
- Focused tests cover the controller/runner behavior where possible, especially distinguishing direct single-Pod launch from multi-owned nested launch.
- `cargo fmt --check`
- `cargo test -p tui multi --no-default-features` or equivalent focused tests.
- `cargo check -p tui -p client -p pod`
## Out of scope
- Dedicated back key.
- Per-Pod detail panes inside the multi-Pod dashboard.
- Protocol changes.
- Changing direct-send semantics.
- Changing Pod visibility/discovery rules.

View File

@ -0,0 +1,80 @@
---
id: 20260528-233524-multi-pod-open-return
slug: multi-pod-open-return
title: Return to multi-Pod view after opening a Pod
status: closed
kind: task
priority: P2
labels: [tui, pod, ux]
created_at: 2026-05-28T23:35:24Z
updated_at: 2026-05-28T23:57:49Z
assignee: null
legacy_ticket: null
---
## Background
`tui --multi` can open the selected Pod with `o`. The current implementation treats this as leaving the multi-Pod dashboard and tail-calling the normal single-Pod TUI. Once the single-Pod screen is detached with `Ctrl+D` / `Ctrl+C`, the process exits instead of returning to the multi-Pod view.
For now, no special "back mode" or dedicated back key is needed. The desired behavior is simpler: when the user opens a Pod from the multi-Pod dashboard, the normal single-Pod attach screen runs as a nested attach session. When that session exits normally by detach/quit (`Ctrl+D`, `Ctrl+C`, or equivalent normal exit), control returns to the multi-Pod dashboard.
This should be implemented by abstracting the TUI launch/control flow, not by adding protocol features or making the single-Pod App deeply aware of multi-Pod mode.
## Requirements
- `tui --multi` remains the entrypoint for the multi-Pod dashboard.
- In `tui --multi`, pressing `o` on a selected openable Pod opens the normal single-Pod conversation screen.
- When that single-Pod screen exits normally, TUI returns to the multi-Pod dashboard instead of exiting the process.
- `Ctrl+D` / `Ctrl+C` detach/quit from the opened single-Pod screen should return to multi view.
- Normal single-Pod launches such as `tui <pod>` / `tui --pod <name>` should continue to exit the process on `Ctrl+D` / `Ctrl+C`.
- Avoid introducing a dedicated back key or back mode for this ticket.
- The caller loop determines whether a normal single-Pod exit returns to multi view or exits the process.
- Preserve multi-Pod dashboard state where practical.
- The selected Pod name should remain selected after returning, if still visible.
- The multi-Pod composer contents should be preserved across open/return.
- The Pod list should refresh after returning so status changes are visible.
- Keep terminal handling clean.
- Do not unnecessarily leave/re-enter alternate screen between multi view and the nested single-Pod screen if the existing terminal can be reused safely.
- If reusing the same `Terminal`, ensure cursor/mouse/raw-mode cleanup remains correct on final exit and on errors.
- Error handling should be explicit.
- If opening the selected Pod fails before the single-Pod screen starts, show a multi-view notice and keep the dashboard active.
- If the single-Pod session exits with a fatal error, return that error or show a clear diagnostic according to the existing TUI error behavior; do not silently swallow fatal failures.
- Existing `tui --multi` direct send behavior, section layout, and separator fix must continue to work.
## Suggested implementation direction
- Split the current single-Pod attach runner into a reusable function that accepts an existing `Terminal` and returns when the attached screen exits.
- Change `run_multi()` from one-shot `multi_pod::run(...).await -> Open -> run_pod_name(...)` into a controller loop:
```text
loop:
run multi dashboard with previous app state / selected Pod
Quit => exit process
Open(pod) => run single-Pod attach screen using the same terminal
on normal exit, refresh dashboard and continue loop
```
- Avoid adding a new protocol method. This is local TUI orchestration.
- Avoid making `App` carry a generic `BackToMulti` mode unless it is strictly necessary; prefer caller-owned control flow.
## Acceptance criteria
- From `tui --multi`, pressing `o` opens the selected Pod's normal conversation screen.
- Pressing `Ctrl+D` / `Ctrl+C` in that opened screen returns to the multi-Pod dashboard.
- Starting a single-Pod TUI directly still exits on `Ctrl+D` / `Ctrl+C`.
- Returning to multi view preserves multi composer contents and selected Pod name when possible.
- Returning to multi view refreshes Pod status/list.
- Opening failure from multi view leaves the user in multi view with a visible notice.
- Existing multi-Pod tests still pass.
- Focused tests cover the controller/runner behavior where possible, especially distinguishing direct single-Pod launch from multi-owned nested launch.
- `cargo fmt --check`
- `cargo test -p tui multi --no-default-features` or equivalent focused tests.
- `cargo check -p tui -p client -p pod`
## Out of scope
- Dedicated back key.
- Per-Pod detail panes inside the multi-Pod dashboard.
- Protocol changes.
- Changing direct-send semantics.
- Changing Pod visibility/discovery rules.

View File

@ -0,0 +1,95 @@
<!-- event: create author: tickets.sh at: 2026-05-28T23:35:24Z -->
## Created
Created by tickets.sh create.
---
<!-- event: close author: hare at: 2026-05-28T23:57:49Z status: closed -->
## Closed
---
id: 20260528-233524-multi-pod-open-return
slug: multi-pod-open-return
title: Return to multi-Pod view after opening a Pod
status: closed
kind: task
priority: P2
labels: [tui, pod, ux]
created_at: 2026-05-28T23:35:24Z
updated_at: 2026-05-28T23:57:49Z
assignee: null
legacy_ticket: null
---
## Background
`tui --multi` can open the selected Pod with `o`. The current implementation treats this as leaving the multi-Pod dashboard and tail-calling the normal single-Pod TUI. Once the single-Pod screen is detached with `Ctrl+D` / `Ctrl+C`, the process exits instead of returning to the multi-Pod view.
For now, no special "back mode" or dedicated back key is needed. The desired behavior is simpler: when the user opens a Pod from the multi-Pod dashboard, the normal single-Pod attach screen runs as a nested attach session. When that session exits normally by detach/quit (`Ctrl+D`, `Ctrl+C`, or equivalent normal exit), control returns to the multi-Pod dashboard.
This should be implemented by abstracting the TUI launch/control flow, not by adding protocol features or making the single-Pod App deeply aware of multi-Pod mode.
## Requirements
- `tui --multi` remains the entrypoint for the multi-Pod dashboard.
- In `tui --multi`, pressing `o` on a selected openable Pod opens the normal single-Pod conversation screen.
- When that single-Pod screen exits normally, TUI returns to the multi-Pod dashboard instead of exiting the process.
- `Ctrl+D` / `Ctrl+C` detach/quit from the opened single-Pod screen should return to multi view.
- Normal single-Pod launches such as `tui <pod>` / `tui --pod <name>` should continue to exit the process on `Ctrl+D` / `Ctrl+C`.
- Avoid introducing a dedicated back key or back mode for this ticket.
- The caller loop determines whether a normal single-Pod exit returns to multi view or exits the process.
- Preserve multi-Pod dashboard state where practical.
- The selected Pod name should remain selected after returning, if still visible.
- The multi-Pod composer contents should be preserved across open/return.
- The Pod list should refresh after returning so status changes are visible.
- Keep terminal handling clean.
- Do not unnecessarily leave/re-enter alternate screen between multi view and the nested single-Pod screen if the existing terminal can be reused safely.
- If reusing the same `Terminal`, ensure cursor/mouse/raw-mode cleanup remains correct on final exit and on errors.
- Error handling should be explicit.
- If opening the selected Pod fails before the single-Pod screen starts, show a multi-view notice and keep the dashboard active.
- If the single-Pod session exits with a fatal error, return that error or show a clear diagnostic according to the existing TUI error behavior; do not silently swallow fatal failures.
- Existing `tui --multi` direct send behavior, section layout, and separator fix must continue to work.
## Suggested implementation direction
- Split the current single-Pod attach runner into a reusable function that accepts an existing `Terminal` and returns when the attached screen exits.
- Change `run_multi()` from one-shot `multi_pod::run(...).await -> Open -> run_pod_name(...)` into a controller loop:
```text
loop:
run multi dashboard with previous app state / selected Pod
Quit => exit process
Open(pod) => run single-Pod attach screen using the same terminal
on normal exit, refresh dashboard and continue loop
```
- Avoid adding a new protocol method. This is local TUI orchestration.
- Avoid making `App` carry a generic `BackToMulti` mode unless it is strictly necessary; prefer caller-owned control flow.
## Acceptance criteria
- From `tui --multi`, pressing `o` opens the selected Pod's normal conversation screen.
- Pressing `Ctrl+D` / `Ctrl+C` in that opened screen returns to the multi-Pod dashboard.
- Starting a single-Pod TUI directly still exits on `Ctrl+D` / `Ctrl+C`.
- Returning to multi view preserves multi composer contents and selected Pod name when possible.
- Returning to multi view refreshes Pod status/list.
- Opening failure from multi view leaves the user in multi view with a visible notice.
- Existing multi-Pod tests still pass.
- Focused tests cover the controller/runner behavior where possible, especially distinguishing direct single-Pod launch from multi-owned nested launch.
- `cargo fmt --check`
- `cargo test -p tui multi --no-default-features` or equivalent focused tests.
- `cargo check -p tui -p client -p pod`
## Out of scope
- Dedicated back key.
- Per-Pod detail panes inside the multi-Pod dashboard.
- Protocol changes.
- Changing direct-send semantics.
- Changing Pod visibility/discovery rules.
---

View File

@ -0,0 +1,63 @@
---
id: 20260529-001326-rename-installed-binaries
slug: rename-installed-binaries
title: Rename installed binaries
status: closed
kind: task
priority: P2
labels: [cli, packaging, tui, pod]
created_at: 2026-05-29T00:13:26Z
updated_at: 2026-05-29T00:39:08Z
assignee: null
legacy_ticket: null
---
## Background
The workspace crate names `tui` and `pod` are useful internally, but the installed command names are too generic for user environments. `tui` does not identify the application, and `pod` collides with common terminology and other tooling.
Use application-specific binary names for installed commands:
- `insomnia`: the main terminal UI / user entrypoint, currently built from the `tui` crate.
- `insomnia-pod`: the Pod CLI/runtime command, currently built from the `pod` crate.
This is a command name change, not a crate rename. Keep the Rust crate/package names `tui` and `pod` unless there is a separate design decision to rename crates.
## Requirements
- Rename Cargo binary outputs:
- `crates/tui` binary name becomes `insomnia`.
- `crates/pod` binary name becomes `insomnia-pod`.
- Do not add legacy `tui` / `pod` installed aliases unless a concrete internal dependency requires it and is documented.
- Update Nix packaging to install and check the new binary names.
- `$out/bin/insomnia`
- `$out/bin/insomnia-pod`
- Update flake apps to use the new command names.
- default app should run `insomnia`.
- expose app entries for `insomnia` and `insomnia-pod`.
- Update docs that instruct users to run `tui` / `pod` as installed commands.
- Keep references to crate/package names where they are explicitly Cargo package names, e.g. `cargo check -p tui`.
- Prefer `cargo run -p tui -- ...` in development docs if referring to crate-based development invocation, but installed usage should use `insomnia`.
- Audit code/tests/scripts for assumptions that installed binary names are `tui` or `pod`.
- Internal runtime process spawning must still work.
- If code intentionally uses Cargo package names, leave them unchanged.
- Keep CLI semantics unchanged except for command names.
## Acceptance criteria
- `cargo build -p tui -p pod` produces runnable binaries named `insomnia` and `insomnia-pod`.
- `cargo run -p tui -- --help` and `cargo run -p pod -- --help` still work as development package invocations.
- Installed/Nix package smoke checks look for `insomnia` and `insomnia-pod`, not `tui` and `pod`.
- `flake.nix` app outputs use the new binary names.
- User-facing docs no longer tell users to run installed commands as `tui` / `pod`.
- No legacy aliases are installed unless explicitly justified.
- `cargo fmt --check`
- Focused cargo checks/tests for affected crates, at least `cargo check -p tui -p pod`.
- Nix validation that does not require network where possible, e.g. `nix flake check --no-build`.
## Out of scope
- Renaming crates/packages from `tui` / `pod`.
- Changing CLI argument semantics.
- Changing Pod protocol or socket behavior.
- Publishing or Home Manager module changes.

View File

@ -0,0 +1,63 @@
---
id: 20260529-001326-rename-installed-binaries
slug: rename-installed-binaries
title: Rename installed binaries
status: closed
kind: task
priority: P2
labels: [cli, packaging, tui, pod]
created_at: 2026-05-29T00:13:26Z
updated_at: 2026-05-29T00:39:08Z
assignee: null
legacy_ticket: null
---
## Background
The workspace crate names `tui` and `pod` are useful internally, but the installed command names are too generic for user environments. `tui` does not identify the application, and `pod` collides with common terminology and other tooling.
Use application-specific binary names for installed commands:
- `insomnia`: the main terminal UI / user entrypoint, currently built from the `tui` crate.
- `insomnia-pod`: the Pod CLI/runtime command, currently built from the `pod` crate.
This is a command name change, not a crate rename. Keep the Rust crate/package names `tui` and `pod` unless there is a separate design decision to rename crates.
## Requirements
- Rename Cargo binary outputs:
- `crates/tui` binary name becomes `insomnia`.
- `crates/pod` binary name becomes `insomnia-pod`.
- Do not add legacy `tui` / `pod` installed aliases unless a concrete internal dependency requires it and is documented.
- Update Nix packaging to install and check the new binary names.
- `$out/bin/insomnia`
- `$out/bin/insomnia-pod`
- Update flake apps to use the new command names.
- default app should run `insomnia`.
- expose app entries for `insomnia` and `insomnia-pod`.
- Update docs that instruct users to run `tui` / `pod` as installed commands.
- Keep references to crate/package names where they are explicitly Cargo package names, e.g. `cargo check -p tui`.
- Prefer `cargo run -p tui -- ...` in development docs if referring to crate-based development invocation, but installed usage should use `insomnia`.
- Audit code/tests/scripts for assumptions that installed binary names are `tui` or `pod`.
- Internal runtime process spawning must still work.
- If code intentionally uses Cargo package names, leave them unchanged.
- Keep CLI semantics unchanged except for command names.
## Acceptance criteria
- `cargo build -p tui -p pod` produces runnable binaries named `insomnia` and `insomnia-pod`.
- `cargo run -p tui -- --help` and `cargo run -p pod -- --help` still work as development package invocations.
- Installed/Nix package smoke checks look for `insomnia` and `insomnia-pod`, not `tui` and `pod`.
- `flake.nix` app outputs use the new binary names.
- User-facing docs no longer tell users to run installed commands as `tui` / `pod`.
- No legacy aliases are installed unless explicitly justified.
- `cargo fmt --check`
- Focused cargo checks/tests for affected crates, at least `cargo check -p tui -p pod`.
- Nix validation that does not require network where possible, e.g. `nix flake check --no-build`.
## Out of scope
- Renaming crates/packages from `tui` / `pod`.
- Changing CLI argument semantics.
- Changing Pod protocol or socket behavior.
- Publishing or Home Manager module changes.

View File

@ -0,0 +1,78 @@
<!-- event: create author: tickets.sh at: 2026-05-29T00:13:26Z -->
## Created
Created by tickets.sh create.
---
<!-- event: close author: hare at: 2026-05-29T00:39:08Z status: closed -->
## Closed
---
id: 20260529-001326-rename-installed-binaries
slug: rename-installed-binaries
title: Rename installed binaries
status: closed
kind: task
priority: P2
labels: [cli, packaging, tui, pod]
created_at: 2026-05-29T00:13:26Z
updated_at: 2026-05-29T00:39:08Z
assignee: null
legacy_ticket: null
---
## Background
The workspace crate names `tui` and `pod` are useful internally, but the installed command names are too generic for user environments. `tui` does not identify the application, and `pod` collides with common terminology and other tooling.
Use application-specific binary names for installed commands:
- `insomnia`: the main terminal UI / user entrypoint, currently built from the `tui` crate.
- `insomnia-pod`: the Pod CLI/runtime command, currently built from the `pod` crate.
This is a command name change, not a crate rename. Keep the Rust crate/package names `tui` and `pod` unless there is a separate design decision to rename crates.
## Requirements
- Rename Cargo binary outputs:
- `crates/tui` binary name becomes `insomnia`.
- `crates/pod` binary name becomes `insomnia-pod`.
- Do not add legacy `tui` / `pod` installed aliases unless a concrete internal dependency requires it and is documented.
- Update Nix packaging to install and check the new binary names.
- `$out/bin/insomnia`
- `$out/bin/insomnia-pod`
- Update flake apps to use the new command names.
- default app should run `insomnia`.
- expose app entries for `insomnia` and `insomnia-pod`.
- Update docs that instruct users to run `tui` / `pod` as installed commands.
- Keep references to crate/package names where they are explicitly Cargo package names, e.g. `cargo check -p tui`.
- Prefer `cargo run -p tui -- ...` in development docs if referring to crate-based development invocation, but installed usage should use `insomnia`.
- Audit code/tests/scripts for assumptions that installed binary names are `tui` or `pod`.
- Internal runtime process spawning must still work.
- If code intentionally uses Cargo package names, leave them unchanged.
- Keep CLI semantics unchanged except for command names.
## Acceptance criteria
- `cargo build -p tui -p pod` produces runnable binaries named `insomnia` and `insomnia-pod`.
- `cargo run -p tui -- --help` and `cargo run -p pod -- --help` still work as development package invocations.
- Installed/Nix package smoke checks look for `insomnia` and `insomnia-pod`, not `tui` and `pod`.
- `flake.nix` app outputs use the new binary names.
- User-facing docs no longer tell users to run installed commands as `tui` / `pod`.
- No legacy aliases are installed unless explicitly justified.
- `cargo fmt --check`
- Focused cargo checks/tests for affected crates, at least `cargo check -p tui -p pod`.
- Nix validation that does not require network where possible, e.g. `nix flake check --no-build`.
## Out of scope
- Renaming crates/packages from `tui` / `pod`.
- Changing CLI argument semantics.
- Changing Pod protocol or socket behavior.
- Publishing or Home Manager module changes.
---

View File

@ -0,0 +1,58 @@
---
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

@ -0,0 +1,58 @@
---
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

@ -0,0 +1,73 @@
<!-- 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

@ -0,0 +1,55 @@
---
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

@ -0,0 +1,55 @@
---
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

@ -0,0 +1,70 @@
<!-- 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

@ -0,0 +1,55 @@
---
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,55 @@
---
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,70 @@
<!-- 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

@ -1,74 +0,0 @@
---
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

@ -1,7 +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.
---

View File

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