@duetso/agent 0.1.81 → 0.1.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/src/memory/observational.d.ts +7 -0
  3. package/dist/src/memory/observational.d.ts.map +1 -1
  4. package/dist/src/memory/observational.js +7 -1
  5. package/dist/src/memory/observational.js.map +1 -1
  6. package/dist/src/tui/app.d.ts +25 -35
  7. package/dist/src/tui/app.d.ts.map +1 -1
  8. package/dist/src/tui/app.js +195 -2308
  9. package/dist/src/tui/app.js.map +1 -1
  10. package/dist/src/tui/autocomplete-controller.d.ts +83 -0
  11. package/dist/src/tui/autocomplete-controller.d.ts.map +1 -0
  12. package/dist/src/tui/autocomplete-controller.js +308 -0
  13. package/dist/src/tui/autocomplete-controller.js.map +1 -0
  14. package/dist/src/tui/boot-screen.d.ts +64 -0
  15. package/dist/src/tui/boot-screen.d.ts.map +1 -0
  16. package/dist/src/tui/boot-screen.js +137 -0
  17. package/dist/src/tui/boot-screen.js.map +1 -0
  18. package/dist/src/tui/controllers.d.ts +39 -0
  19. package/dist/src/tui/controllers.d.ts.map +1 -0
  20. package/dist/src/tui/controllers.js +53 -0
  21. package/dist/src/tui/controllers.js.map +1 -0
  22. package/dist/src/tui/copy-controller.d.ts +75 -0
  23. package/dist/src/tui/copy-controller.d.ts.map +1 -0
  24. package/dist/src/tui/copy-controller.js +140 -0
  25. package/dist/src/tui/copy-controller.js.map +1 -0
  26. package/dist/src/tui/history-replay.d.ts +33 -0
  27. package/dist/src/tui/history-replay.d.ts.map +1 -0
  28. package/dist/src/tui/history-replay.js +47 -0
  29. package/dist/src/tui/history-replay.js.map +1 -0
  30. package/dist/src/tui/initial-prompt.d.ts +24 -0
  31. package/dist/src/tui/initial-prompt.d.ts.map +1 -0
  32. package/dist/src/tui/initial-prompt.js +29 -0
  33. package/dist/src/tui/initial-prompt.js.map +1 -0
  34. package/dist/src/tui/key-handlers.d.ts +49 -0
  35. package/dist/src/tui/key-handlers.d.ts.map +1 -0
  36. package/dist/src/tui/key-handlers.js +192 -0
  37. package/dist/src/tui/key-handlers.js.map +1 -0
  38. package/dist/src/tui/layout.d.ts +71 -0
  39. package/dist/src/tui/layout.d.ts.map +1 -0
  40. package/dist/src/tui/layout.js +245 -0
  41. package/dist/src/tui/layout.js.map +1 -0
  42. package/dist/src/tui/paste-controller.d.ts +90 -0
  43. package/dist/src/tui/paste-controller.d.ts.map +1 -0
  44. package/dist/src/tui/paste-controller.js +234 -0
  45. package/dist/src/tui/paste-controller.js.map +1 -0
  46. package/dist/src/tui/question-picker.d.ts +111 -0
  47. package/dist/src/tui/question-picker.d.ts.map +1 -0
  48. package/dist/src/tui/question-picker.js +308 -0
  49. package/dist/src/tui/question-picker.js.map +1 -0
  50. package/dist/src/tui/renderer-lifecycle.d.ts +20 -0
  51. package/dist/src/tui/renderer-lifecycle.d.ts.map +1 -0
  52. package/dist/src/tui/renderer-lifecycle.js +50 -0
  53. package/dist/src/tui/renderer-lifecycle.js.map +1 -0
  54. package/dist/src/tui/session-subscription.d.ts +30 -0
  55. package/dist/src/tui/session-subscription.d.ts.map +1 -0
  56. package/dist/src/tui/session-subscription.js +78 -0
  57. package/dist/src/tui/session-subscription.js.map +1 -0
  58. package/dist/src/tui/slash-commands.d.ts +23 -0
  59. package/dist/src/tui/slash-commands.d.ts.map +1 -0
  60. package/dist/src/tui/slash-commands.js +79 -0
  61. package/dist/src/tui/slash-commands.js.map +1 -0
  62. package/dist/src/tui/starter-section.d.ts +99 -0
  63. package/dist/src/tui/starter-section.d.ts.map +1 -0
  64. package/dist/src/tui/starter-section.js +254 -0
  65. package/dist/src/tui/starter-section.js.map +1 -0
  66. package/dist/src/tui/status-controller.d.ts +94 -0
  67. package/dist/src/tui/status-controller.d.ts.map +1 -0
  68. package/dist/src/tui/status-controller.js +186 -0
  69. package/dist/src/tui/status-controller.js.map +1 -0
  70. package/dist/src/tui/step-renderer.d.ts +55 -0
  71. package/dist/src/tui/step-renderer.d.ts.map +1 -0
  72. package/dist/src/tui/step-renderer.js +246 -0
  73. package/dist/src/tui/step-renderer.js.map +1 -0
  74. package/dist/src/tui/transcript-writer.d.ts +63 -0
  75. package/dist/src/tui/transcript-writer.d.ts.map +1 -0
  76. package/dist/src/tui/transcript-writer.js +130 -0
  77. package/dist/src/tui/transcript-writer.js.map +1 -0
  78. package/dist/src/turn-runner/turn-runner.d.ts +69 -3
  79. package/dist/src/turn-runner/turn-runner.d.ts.map +1 -1
  80. package/dist/src/turn-runner/turn-runner.js +123 -20
  81. package/dist/src/turn-runner/turn-runner.js.map +1 -1
  82. package/package.json +1 -1
@@ -0,0 +1,64 @@
1
+ import { type CliRenderer, type ScrollBoxRenderable } from "@opentui/core";
2
+ import { type UpgradeStatusStream } from "../cli/auto-upgrade.js";
3
+ import type { Session } from "../session/session.js";
4
+ import type { TurnAgentFile } from "../types/protocol.js";
5
+ import type { AutocompleteController } from "./autocomplete-controller.js";
6
+ import type { Sidebar } from "./sidebar.js";
7
+ import type { StarterSection } from "./starter-section.js";
8
+ /**
9
+ * Inputs to the boot intro renderer. Everything is read-only — the boot
10
+ * screen is a one-shot painter that runs after chrome is constructed and
11
+ * before the first turn dispatches.
12
+ */
13
+ export interface BootScreenDeps {
14
+ renderer: CliRenderer;
15
+ transcript: ScrollBoxRenderable;
16
+ appendLine(content: string, fg: string): void;
17
+ packageName: string;
18
+ packageVersion: string;
19
+ workDir: string;
20
+ modelName: string;
21
+ memoryModelName: string;
22
+ upgradeStatus$?: UpgradeStatusStream;
23
+ /**
24
+ * Present only on fresh (non-resume) mounts. `runTui` creates the
25
+ * StarterSection lazily and leaves it undefined on resume mounts, so the
26
+ * starter chrome is skipped without needing a separate resume flag.
27
+ */
28
+ starters?: StarterSection;
29
+ }
30
+ /**
31
+ * Paints the boot banner, header, optional upgrade-status line, agent-file
32
+ * summary, and (on fresh boots) mounts the starter chrome.
33
+ */
34
+ export declare function renderSetupIntro(deps: BootScreenDeps, skills: ReadonlyArray<{
35
+ name: string;
36
+ }>, agentFiles: readonly TurnAgentFile[]): void;
37
+ export declare function formatBootHeader(deps: {
38
+ packageVersion: string;
39
+ workDir: string;
40
+ modelName: string;
41
+ memoryModelName: string;
42
+ }): string;
43
+ /**
44
+ * One-shot boot intro: loads skills + agent files from the session, seeds
45
+ * the autocomplete index, paints the banner/header/agent-file line, and
46
+ * refreshes the sidebar from the session's current state snapshot.
47
+ */
48
+ export declare function renderBootScreen(deps: {
49
+ renderer: CliRenderer;
50
+ transcript: ScrollBoxRenderable;
51
+ appendLine(content: string, fg: string): void;
52
+ session: Session;
53
+ sidebar: Sidebar;
54
+ autocomplete: AutocompleteController;
55
+ starters?: StarterSection;
56
+ packageName: string;
57
+ packageVersion: string;
58
+ workDir: string;
59
+ modelName: string;
60
+ memoryModelName: string;
61
+ upgradeStatus$?: UpgradeStatusStream;
62
+ }): Promise<void>;
63
+ export declare function shortenCwd(cwd: string): string;
64
+ //# sourceMappingURL=boot-screen.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boot-screen.d.ts","sourceRoot":"","sources":["../../../src/tui/boot-screen.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,mBAAmB,EAAkB,MAAM,eAAe,CAAC;AAE3F,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAG3E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAI3D;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,WAAW,CAAC;IACtB,UAAU,EAAE,mBAAmB,CAAC;IAChC,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,mBAAmB,CAAC;IACrC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EACvC,UAAU,EAAE,SAAS,aAAa,EAAE,GACnC,IAAI,CA6BN;AAmDD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IACrC,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;CACzB,GAAG,MAAM,CAGT;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,QAAQ,EAAE,WAAW,CAAC;IACtB,UAAU,EAAE,mBAAmB,CAAC;IAChC,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,sBAAsB,CAAC;IACrC,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,mBAAmB,CAAC;CACtC,GAAG,OAAO,CAAC,IAAI,CAAC,CAgChB;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAK9C"}
@@ -0,0 +1,137 @@
1
+ import { TextRenderable } from "@opentui/core";
2
+ import { homedir } from "node:os";
3
+ import { describeUpgradeStatus, } from "../cli/auto-upgrade.js";
4
+ import { BUILT_IN_SLASH_COMMANDS } from "./autocomplete.js";
5
+ import { refreshSidebarFromSession } from "./session-subscription.js";
6
+ import { DUET_BANNER_LINES_COMPACT } from "./history.js";
7
+ import { COLORS } from "./theme.js";
8
+ /**
9
+ * Paints the boot banner, header, optional upgrade-status line, agent-file
10
+ * summary, and (on fresh boots) mounts the starter chrome.
11
+ */
12
+ export function renderSetupIntro(deps, skills, agentFiles) {
13
+ // Compact 3-row wordmark. The full DUET_BANNER_LINES is ~6 rows tall and
14
+ // pushed the starter list off-screen on small terminals; this one keeps
15
+ // the brand mark visible while leaving room for the starter prompts to
16
+ // land above the fold.
17
+ for (const line of DUET_BANNER_LINES_COMPACT)
18
+ deps.appendLine(line, COLORS.status);
19
+ deps.appendLine(" ", COLORS.hint);
20
+ deps.appendLine(" ", COLORS.hint);
21
+ // One-line header. Keeps cwd/model context visible without burning another
22
+ // five rows. Provenance (env/file source) is intentionally dropped here —
23
+ // surface it via /whoami later.
24
+ deps.appendLine(formatBootHeader(deps), COLORS.status);
25
+ if (deps.upgradeStatus$) {
26
+ subscribeUpgradeStatus(deps);
27
+ }
28
+ // Only mention agent files when one is actually loaded; "[agent file]
29
+ // none" is noise on every empty boot.
30
+ if (agentFiles.length > 0) {
31
+ deps.appendLine(`[agent file] ${agentFiles.map((file) => file.name).join(", ")}`, COLORS.hint);
32
+ }
33
+ // Resume mounts leave `deps.starters` undefined so we skip the "what
34
+ // should we work on today?" menu — the user explicitly asked to drop
35
+ // back into a known conversation.
36
+ if (deps.starters) {
37
+ deps.starters.mount(skills);
38
+ }
39
+ }
40
+ function subscribeUpgradeStatus(deps) {
41
+ const stream = deps.upgradeStatus$;
42
+ if (!stream)
43
+ return;
44
+ // Lazy construction on the first status that has human-readable text.
45
+ // Statuses without text (current/locked/skipped) skip the constructor
46
+ // entirely; constructing eagerly would allocate a native text buffer
47
+ // against the renderer that we would never `destroy()` on the silent path.
48
+ //
49
+ // `subscribe()` replays the latest status synchronously, so the handler
50
+ // runs before `subscribe()` returns its unsubscribe handle. We set a
51
+ // `done` flag from inside the handler and tear down after `subscribe()`
52
+ // returns; subsequent (async) terminal statuses unsubscribe inline via
53
+ // the real handle.
54
+ let upgradeLine;
55
+ let done = false;
56
+ let unsubscribe = () => { };
57
+ const handle = (status) => {
58
+ const text = describeUpgradeStatus(deps.packageName, status);
59
+ if (!text) {
60
+ if (upgradeLine) {
61
+ deps.transcript.remove(upgradeLine.id);
62
+ upgradeLine.destroy();
63
+ upgradeLine = undefined;
64
+ }
65
+ // Terminal statuses with no human-readable form (current, locked,
66
+ // skipped) close the subscription so we stop reacting.
67
+ if (status.kind !== "checking") {
68
+ done = true;
69
+ unsubscribe();
70
+ }
71
+ return;
72
+ }
73
+ const fg = status.kind === "failed" ? COLORS.error : COLORS.system;
74
+ if (!upgradeLine) {
75
+ upgradeLine = new TextRenderable(deps.renderer, { content: `[update] ${text}`, fg });
76
+ deps.transcript.add(upgradeLine);
77
+ }
78
+ else {
79
+ upgradeLine.content = `[update] ${text}`;
80
+ upgradeLine.fg = fg;
81
+ }
82
+ if (status.kind === "upgraded" || status.kind === "failed") {
83
+ done = true;
84
+ unsubscribe();
85
+ }
86
+ };
87
+ unsubscribe = stream.subscribe(handle);
88
+ if (done)
89
+ unsubscribe();
90
+ }
91
+ export function formatBootHeader(deps) {
92
+ const cwdLabel = shortenCwd(deps.workDir);
93
+ return `DUET AGENT v${deps.packageVersion} · ${cwdLabel} · ${deps.modelName} + ${deps.memoryModelName}`;
94
+ }
95
+ /**
96
+ * One-shot boot intro: loads skills + agent files from the session, seeds
97
+ * the autocomplete index, paints the banner/header/agent-file line, and
98
+ * refreshes the sidebar from the session's current state snapshot.
99
+ */
100
+ export async function renderBootScreen(deps) {
101
+ const [skills, agentFiles] = await Promise.all([
102
+ deps.session.getSkills(),
103
+ deps.session.getResolvedAgentFiles(),
104
+ ]);
105
+ deps.autocomplete.setSkillItems([
106
+ ...BUILT_IN_SLASH_COMMANDS,
107
+ ...skills.map((skill) => ({
108
+ name: skill.name,
109
+ description: skill.description,
110
+ path: skill.baseDir,
111
+ group: "skills",
112
+ })),
113
+ ]);
114
+ deps.autocomplete.refresh();
115
+ renderSetupIntro({
116
+ renderer: deps.renderer,
117
+ transcript: deps.transcript,
118
+ appendLine: deps.appendLine,
119
+ packageName: deps.packageName,
120
+ packageVersion: deps.packageVersion,
121
+ workDir: deps.workDir,
122
+ modelName: deps.modelName,
123
+ memoryModelName: deps.memoryModelName,
124
+ upgradeStatus$: deps.upgradeStatus$,
125
+ starters: deps.starters,
126
+ }, skills, agentFiles);
127
+ refreshSidebarFromSession({ session: deps.session, sidebar: deps.sidebar });
128
+ }
129
+ export function shortenCwd(cwd) {
130
+ const home = homedir();
131
+ if (cwd === home)
132
+ return "~";
133
+ if (cwd.startsWith(`${home}/`))
134
+ return `~${cwd.slice(home.length)}`;
135
+ return cwd;
136
+ }
137
+ //# sourceMappingURL=boot-screen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boot-screen.js","sourceRoot":"","sources":["../../../src/tui/boot-screen.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import type { CliRenderer } from "@opentui/core";
2
+ import { AutocompleteController } from "./autocomplete-controller.js";
3
+ import { CopyController } from "./copy-controller.js";
4
+ import type { LayoutRefs } from "./layout.js";
5
+ import { PasteController } from "./paste-controller.js";
6
+ import { QuestionPicker } from "./question-picker.js";
7
+ import type { StatusController } from "./status-controller.js";
8
+ import type { TranscriptEntry } from "./transcript-log.js";
9
+ import type { TranscriptWriter } from "./transcript-writer.js";
10
+ import type { Session } from "../session/session.js";
11
+ export interface TuiControllers {
12
+ autocomplete: AutocompleteController;
13
+ questionPicker: QuestionPicker;
14
+ pasteController: PasteController;
15
+ copyController: CopyController;
16
+ }
17
+ export interface TuiControllerDeps {
18
+ renderer: CliRenderer;
19
+ ui: LayoutRefs;
20
+ session: Session;
21
+ sessionId: string;
22
+ workDir: string;
23
+ transcriptWriter: TranscriptWriter;
24
+ statusController: StatusController;
25
+ appendBlock(label: string | null, body: string, fg: string): void;
26
+ recordTranscriptEntry(kind: TranscriptEntry["kind"], text: string): void;
27
+ reportError(error: unknown): void;
28
+ onPickerEscapeClose(): void;
29
+ getLastSelectionText(): string;
30
+ clearLastSelectionText(): void;
31
+ }
32
+ /**
33
+ * Bundles the four secondary input controllers that `runTui` wires onto the
34
+ * composer. Kept together because they all share the same layout handles and
35
+ * status/writer pair; building them in one place keeps `runTui` short and
36
+ * makes the controller surface easy to mock in isolation.
37
+ */
38
+ export declare function createTuiControllers(deps: TuiControllerDeps): TuiControllers;
39
+ //# sourceMappingURL=controllers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controllers.d.ts","sourceRoot":"","sources":["../../../src/tui/controllers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AACtE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAErD,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,sBAAsB,CAAC;IACrC,cAAc,EAAE,cAAc,CAAC;IAC/B,eAAe,EAAE,eAAe,CAAC;IACjC,cAAc,EAAE,cAAc,CAAC;CAChC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,WAAW,CAAC;IACtB,EAAE,EAAE,UAAU,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClE,qBAAqB,CAAC,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzE,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAClC,mBAAmB,IAAI,IAAI,CAAC;IAC5B,oBAAoB,IAAI,MAAM,CAAC;IAC/B,sBAAsB,IAAI,IAAI,CAAC;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,iBAAiB,GAAG,cAAc,CA6C5E"}
@@ -0,0 +1,53 @@
1
+ import { AutocompleteController } from "./autocomplete-controller.js";
2
+ import { CopyController } from "./copy-controller.js";
3
+ import { PasteController } from "./paste-controller.js";
4
+ import { QuestionPicker } from "./question-picker.js";
5
+ /**
6
+ * Bundles the four secondary input controllers that `runTui` wires onto the
7
+ * composer. Kept together because they all share the same layout handles and
8
+ * status/writer pair; building them in one place keeps `runTui` short and
9
+ * makes the controller surface easy to mock in isolation.
10
+ */
11
+ export function createTuiControllers(deps) {
12
+ const autocomplete = new AutocompleteController({
13
+ inputField: deps.ui.inputField,
14
+ skillAutocompletePanel: deps.ui.skillAutocompletePanel,
15
+ commandRows: deps.ui.commandRows,
16
+ commandHeader: deps.ui.commandHeader,
17
+ skillRows: deps.ui.skillRows,
18
+ skillHeader: deps.ui.skillHeader,
19
+ fileAutocompletePanel: deps.ui.fileAutocompletePanel,
20
+ fileAutocompleteRows: deps.ui.fileAutocompleteRows,
21
+ workDir: deps.workDir,
22
+ onEscapeClose: deps.onPickerEscapeClose,
23
+ });
24
+ const questionPicker = new QuestionPicker({
25
+ questionPanel: deps.ui.questionPanel,
26
+ questionTitle: deps.ui.questionTitle,
27
+ questionRows: deps.ui.questionRows,
28
+ inputField: deps.ui.inputField,
29
+ session: deps.session,
30
+ onEscapeClose: deps.onPickerEscapeClose,
31
+ appendBlock: deps.appendBlock,
32
+ recordTranscriptEntry: deps.recordTranscriptEntry,
33
+ reportError: deps.reportError,
34
+ markRunning: () => deps.statusController.markRunning(),
35
+ isRunning: () => deps.statusController.isRunning(),
36
+ });
37
+ const pasteController = new PasteController({
38
+ inputField: deps.ui.inputField,
39
+ sessionId: deps.sessionId,
40
+ workDir: deps.workDir,
41
+ appendBlock: deps.appendBlock,
42
+ statusController: deps.statusController,
43
+ });
44
+ const copyController = new CopyController({
45
+ renderer: deps.renderer,
46
+ transcriptWriter: deps.transcriptWriter,
47
+ statusController: deps.statusController,
48
+ getLastSelectionText: deps.getLastSelectionText,
49
+ clearLastSelectionText: deps.clearLastSelectionText,
50
+ });
51
+ return { autocomplete, questionPicker, pasteController, copyController };
52
+ }
53
+ //# sourceMappingURL=controllers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controllers.js","sourceRoot":"","sources":["../../../src/tui/controllers.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AACtE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AA6BtD;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAuB;IAC1D,MAAM,YAAY,GAAG,IAAI,sBAAsB,CAAC;QAC9C,UAAU,EAAE,IAAI,CAAC,EAAE,CAAC,UAAU;QAC9B,sBAAsB,EAAE,IAAI,CAAC,EAAE,CAAC,sBAAsB;QACtD,WAAW,EAAE,IAAI,CAAC,EAAE,CAAC,WAAW;QAChC,aAAa,EAAE,IAAI,CAAC,EAAE,CAAC,aAAa;QACpC,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC,SAAS;QAC5B,WAAW,EAAE,IAAI,CAAC,EAAE,CAAC,WAAW;QAChC,qBAAqB,EAAE,IAAI,CAAC,EAAE,CAAC,qBAAqB;QACpD,oBAAoB,EAAE,IAAI,CAAC,EAAE,CAAC,oBAAoB;QAClD,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,aAAa,EAAE,IAAI,CAAC,mBAAmB;KACxC,CAAC,CAAC;IAEH,MAAM,cAAc,GAAG,IAAI,cAAc,CAAC;QACxC,aAAa,EAAE,IAAI,CAAC,EAAE,CAAC,aAAa;QACpC,aAAa,EAAE,IAAI,CAAC,EAAE,CAAC,aAAa;QACpC,YAAY,EAAE,IAAI,CAAC,EAAE,CAAC,YAAY;QAClC,UAAU,EAAE,IAAI,CAAC,EAAE,CAAC,UAAU;QAC9B,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,aAAa,EAAE,IAAI,CAAC,mBAAmB;QACvC,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,qBAAqB,EAAE,IAAI,CAAC,qBAAqB;QACjD,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,WAAW,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,WAAW,EAAE;QACtD,SAAS,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,SAAS,EAAE;KACnD,CAAC,CAAC;IAEH,MAAM,eAAe,GAAG,IAAI,eAAe,CAAC;QAC1C,UAAU,EAAE,IAAI,CAAC,EAAE,CAAC,UAAU;QAC9B,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;KACxC,CAAC,CAAC;IAEH,MAAM,cAAc,GAAG,IAAI,cAAc,CAAC;QACxC,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;QACvC,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;QACvC,oBAAoB,EAAE,IAAI,CAAC,oBAAoB;QAC/C,sBAAsB,EAAE,IAAI,CAAC,sBAAsB;KACpD,CAAC,CAAC;IAEH,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,CAAC;AAC3E,CAAC"}
@@ -0,0 +1,75 @@
1
+ import { type CliRenderer, type KeyEvent } from "@opentui/core";
2
+ import { type ClipboardWriteResult } from "./clipboard.js";
3
+ import type { StatusController } from "./status-controller.js";
4
+ import type { TranscriptWriter } from "./transcript-writer.js";
5
+ export interface CopyControllerOptions {
6
+ /** Renderer that exposes the OSC 52 fallback writer and the
7
+ * drag-selection clear hook invoked after a successful copy. */
8
+ renderer: CliRenderer;
9
+ /** Transcript writer used to surface copy outcomes (success summary,
10
+ * clipboard write failure) and to source `/copy last|all|<N>` payloads
11
+ * from the message log. */
12
+ transcriptWriter: TranscriptWriter;
13
+ /** Status surface owns the selection-aware hint segment; cleared after a
14
+ * successful keystroke/slash-command copy so the hint drops the
15
+ * copy-keystroke advertisement. */
16
+ statusController: Pick<StatusController, "setSelectionText">;
17
+ /** Reads the most recent drag-selected text. The selection listener still
18
+ * lives in `runTui` so transcript-writer diagnostics see it on the same
19
+ * tick; the controller pulls it through this getter at copy time. */
20
+ getLastSelectionText: () => string;
21
+ /** Clears the cached drag-selection after a successful keystroke copy so
22
+ * subsequent keystrokes do not re-copy stale text. */
23
+ clearLastSelectionText: () => void;
24
+ }
25
+ /**
26
+ * Owns every path that moves text onto the OS clipboard: the platform copy
27
+ * keystroke (Cmd+C / Cmd+Shift+C / Ctrl+Shift+C), the `/copy` slash command,
28
+ * and the underlying two-stage writer (CLI first, OSC 52 fallback).
29
+ *
30
+ * Two-stage clipboard write rationale: the platform-native CLI (pbcopy /
31
+ * wl-copy / xclip / xsel / clip.exe) goes first because `writeClipboardText`
32
+ * reads the clipboard back to confirm the bytes landed — exit-code-only
33
+ * success is not enough (pbcopy from inside a raw-mode TUI on Warp/macOS
34
+ * exits 0 without updating NSPasteboard). OSC 52 is only the fallback when
35
+ * no local CLI exists (SSH session with no clipboard tool installed
36
+ * remotely); when a CLI ran but failed verification we surface that error
37
+ * directly instead of falling through to OSC 52, because OSC 52 would also
38
+ * silently "succeed" on the same broken terminals and hide the real failure.
39
+ */
40
+ export declare class CopyController {
41
+ private readonly opts;
42
+ constructor(opts: CopyControllerOptions);
43
+ /**
44
+ * Detect the platform copy keystroke and, when a non-empty drag-selection
45
+ * exists, route to `copyActiveSelection`. Returns true when the keystroke
46
+ * was consumed (caller must not fall through to Esc/other handlers) and
47
+ * false otherwise so the caller can keep processing.
48
+ *
49
+ * Accepts both "c" and "C" as the key name because some kitty parsers
50
+ * report the shifted letter while others report the base letter with
51
+ * `shift: true`.
52
+ */
53
+ handleCopyKeystroke(key: KeyEvent): boolean;
54
+ /**
55
+ * Copy the active drag-selection to the clipboard and clear the highlight
56
+ * so the user gets visual confirmation the action happened. Used by the
57
+ * platform copy keystroke; the slash command path goes through
58
+ * `handleCopySlashCommand` so it can also serve `/copy last|all|<N>`.
59
+ */
60
+ copyActiveSelection(): Promise<void>;
61
+ /**
62
+ * Resolve a `/copy ...` invocation to clipboard text and pipe it to the
63
+ * OS clipboard. When the user has an active drag-selection and ran a bare
64
+ * `/copy`, copy that highlight verbatim — it matches what they actually
65
+ * have on screen. Otherwise fall back to the transcript-log heuristic
66
+ * (`last` / `all` / `<N>`).
67
+ *
68
+ * Failures are surfaced in the transcript so users on minimal Linux
69
+ * installs see exactly which writer is missing.
70
+ */
71
+ handleCopySlashCommand(raw: string): Promise<void>;
72
+ copyTextToClipboard(text: string): Promise<ClipboardWriteResult>;
73
+ describeCopySelection(argument: "last" | "all" | number, length: number): string;
74
+ }
75
+ //# sourceMappingURL=copy-controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copy-controller.d.ts","sourceRoot":"","sources":["../../../src/tui/copy-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEhE,OAAO,EAAE,KAAK,oBAAoB,EAAsB,MAAM,gBAAgB,CAAC;AAC/E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,MAAM,WAAW,qBAAqB;IACpC;qEACiE;IACjE,QAAQ,EAAE,WAAW,CAAC;IACtB;;gCAE4B;IAC5B,gBAAgB,EAAE,gBAAgB,CAAC;IACnC;;wCAEoC;IACpC,gBAAgB,EAAE,IAAI,CAAC,gBAAgB,EAAE,kBAAkB,CAAC,CAAC;IAC7D;;0EAEsE;IACtE,oBAAoB,EAAE,MAAM,MAAM,CAAC;IACnC;2DACuD;IACvD,sBAAsB,EAAE,MAAM,IAAI,CAAC;CACpC;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,qBAAqB;IAExD;;;;;;;;;OASG;IACH,mBAAmB,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO;IAc3C;;;;;OAKG;IACG,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IAuB1C;;;;;;;;;OASG;IACG,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+ClD,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAiBtE,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;CAMjF"}
@@ -0,0 +1,140 @@
1
+ import { writeClipboardText } from "./clipboard.js";
2
+ import { COLORS } from "./theme.js";
3
+ import { parseCopyArgument, selectCopyText } from "./transcript-log.js";
4
+ /**
5
+ * Owns every path that moves text onto the OS clipboard: the platform copy
6
+ * keystroke (Cmd+C / Cmd+Shift+C / Ctrl+Shift+C), the `/copy` slash command,
7
+ * and the underlying two-stage writer (CLI first, OSC 52 fallback).
8
+ *
9
+ * Two-stage clipboard write rationale: the platform-native CLI (pbcopy /
10
+ * wl-copy / xclip / xsel / clip.exe) goes first because `writeClipboardText`
11
+ * reads the clipboard back to confirm the bytes landed — exit-code-only
12
+ * success is not enough (pbcopy from inside a raw-mode TUI on Warp/macOS
13
+ * exits 0 without updating NSPasteboard). OSC 52 is only the fallback when
14
+ * no local CLI exists (SSH session with no clipboard tool installed
15
+ * remotely); when a CLI ran but failed verification we surface that error
16
+ * directly instead of falling through to OSC 52, because OSC 52 would also
17
+ * silently "succeed" on the same broken terminals and hide the real failure.
18
+ */
19
+ export class CopyController {
20
+ opts;
21
+ constructor(opts) {
22
+ this.opts = opts;
23
+ }
24
+ /**
25
+ * Detect the platform copy keystroke and, when a non-empty drag-selection
26
+ * exists, route to `copyActiveSelection`. Returns true when the keystroke
27
+ * was consumed (caller must not fall through to Esc/other handlers) and
28
+ * false otherwise so the caller can keep processing.
29
+ *
30
+ * Accepts both "c" and "C" as the key name because some kitty parsers
31
+ * report the shifted letter while others report the base letter with
32
+ * `shift: true`.
33
+ */
34
+ handleCopyKeystroke(key) {
35
+ const isCopyLetter = key.name === "c" || key.name === "C";
36
+ if (!isCopyLetter)
37
+ return false;
38
+ const cmdHeld = key.super || key.meta;
39
+ const isCmdC = cmdHeld && !key.shift && !key.ctrl;
40
+ const isCmdShiftC = cmdHeld && key.shift && !key.ctrl;
41
+ const isCtrlShiftC = key.ctrl && key.shift && !cmdHeld;
42
+ if (!(isCmdC || isCmdShiftC || isCtrlShiftC))
43
+ return false;
44
+ if (this.opts.getLastSelectionText().trim().length === 0)
45
+ return false;
46
+ key.preventDefault();
47
+ void this.copyActiveSelection();
48
+ return true;
49
+ }
50
+ /**
51
+ * Copy the active drag-selection to the clipboard and clear the highlight
52
+ * so the user gets visual confirmation the action happened. Used by the
53
+ * platform copy keystroke; the slash command path goes through
54
+ * `handleCopySlashCommand` so it can also serve `/copy last|all|<N>`.
55
+ */
56
+ async copyActiveSelection() {
57
+ const text = this.opts.getLastSelectionText();
58
+ if (text.trim().length === 0)
59
+ return;
60
+ const result = await this.copyTextToClipboard(text);
61
+ this.opts.renderer.clearSelection();
62
+ this.opts.clearLastSelectionText();
63
+ this.opts.statusController.setSelectionText("");
64
+ if (result.ok) {
65
+ this.opts.transcriptWriter.appendBlock("[copy]", `copied selection (${text.length} char${text.length === 1 ? "" : "s"}) to clipboard via ${result.via}`, COLORS.system);
66
+ }
67
+ else {
68
+ this.opts.transcriptWriter.appendBlock("[copy]", `clipboard write failed: ${result.error ?? "unknown error"}` +
69
+ (process.platform === "linux" ? "\nInstall one of: wl-clipboard, xclip, xsel" : ""), COLORS.error);
70
+ }
71
+ }
72
+ /**
73
+ * Resolve a `/copy ...` invocation to clipboard text and pipe it to the
74
+ * OS clipboard. When the user has an active drag-selection and ran a bare
75
+ * `/copy`, copy that highlight verbatim — it matches what they actually
76
+ * have on screen. Otherwise fall back to the transcript-log heuristic
77
+ * (`last` / `all` / `<N>`).
78
+ *
79
+ * Failures are surfaced in the transcript so users on minimal Linux
80
+ * installs see exactly which writer is missing.
81
+ */
82
+ async handleCopySlashCommand(raw) {
83
+ const argumentRaw = raw === "/copy" ? "" : raw.slice("/copy ".length);
84
+ const argument = parseCopyArgument(argumentRaw);
85
+ if (argument === undefined) {
86
+ this.opts.transcriptWriter.appendBlock("[copy]", "Usage: /copy [last|all|<N>] — last (default) copies the most recent agent reply, " +
87
+ "or copies the active drag-selection when one is present", COLORS.system);
88
+ return;
89
+ }
90
+ // A bare `/copy` (or the copy keystroke while a selection is active)
91
+ // prefers the drag-selection so the clipboard matches what the user
92
+ // has highlighted on screen; an explicit `/copy last|all|<N>` always
93
+ // uses the transcript log instead.
94
+ const explicitArgument = argumentRaw.trim().length > 0;
95
+ const selection = this.opts.getLastSelectionText();
96
+ const useSelection = !explicitArgument && selection.trim().length > 0;
97
+ const text = useSelection
98
+ ? selection
99
+ : selectCopyText(this.opts.transcriptWriter.entries(), argument);
100
+ if (!text) {
101
+ this.opts.transcriptWriter.appendBlock("[copy]", "nothing to copy yet", COLORS.system);
102
+ return;
103
+ }
104
+ const result = await this.copyTextToClipboard(text);
105
+ if (result.ok) {
106
+ const summary = useSelection
107
+ ? `selection (${text.length} char${text.length === 1 ? "" : "s"})`
108
+ : this.describeCopySelection(argument, text.length);
109
+ this.opts.transcriptWriter.appendBlock("[copy]", `copied ${summary} to clipboard via ${result.via}`, COLORS.system);
110
+ }
111
+ else {
112
+ this.opts.transcriptWriter.appendBlock("[copy]", `clipboard write failed: ${result.error ?? "unknown error"}` +
113
+ (process.platform === "linux" ? "\nInstall one of: wl-clipboard, xclip, xsel" : ""), COLORS.error);
114
+ }
115
+ }
116
+ async copyTextToClipboard(text) {
117
+ const cli = await writeClipboardText(text);
118
+ if (cli.ok)
119
+ return cli;
120
+ // Only fall back to OSC 52 when the CLI was simply unavailable. If a
121
+ // CLI ran but the readback did not match (cli.kind ===
122
+ // "verification-failed"), OSC 52 is on the same broken pipe and would
123
+ // silently "succeed" the same way — surface the real error.
124
+ if (cli.kind === "no-writer" &&
125
+ this.opts.renderer.isOsc52Supported() &&
126
+ this.opts.renderer.copyToClipboardOSC52(text)) {
127
+ return { ok: true, via: "OSC 52" };
128
+ }
129
+ return cli;
130
+ }
131
+ describeCopySelection(argument, length) {
132
+ const chars = `${length} char${length === 1 ? "" : "s"}`;
133
+ if (argument === "last")
134
+ return `last message (${chars})`;
135
+ if (argument === "all")
136
+ return `full transcript (${chars})`;
137
+ return `last ${argument} messages (${chars})`;
138
+ }
139
+ }
140
+ //# sourceMappingURL=copy-controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copy-controller.js","sourceRoot":"","sources":["../../../src/tui/copy-controller.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
+ import type { TranscriptEntry } from "./transcript-log.js";
3
+ import { type HistoryDisplayBlock } from "./history.js";
4
+ /**
5
+ * Inputs to the resume replay step. Pulled out so the closures inside `runTui`
6
+ * stay short and the (stateless) coordinator is testable in isolation.
7
+ */
8
+ export interface HistoryReplayDeps {
9
+ appendLine(content: string, fg: string): void;
10
+ appendBlock(label: string | null, body: string, fg: string): void;
11
+ recordTranscriptEntry(kind: TranscriptEntry["kind"], text: string): void;
12
+ }
13
+ /**
14
+ * Subset of `RunTuiInput` consumed by the replay coordinator. Kept structural
15
+ * so callers can pass `RunTuiInput` directly without an adapter.
16
+ */
17
+ export interface HistoryReplayInput {
18
+ history?: AgentMessage[];
19
+ /**
20
+ * Trailing user-turn exchanges to surface in the visible transcript.
21
+ * Undefined means "render everything"; `0` disables visible replay but
22
+ * still seeds the copy-out log below.
23
+ */
24
+ resumeHistoryMessages?: number;
25
+ }
26
+ /**
27
+ * Paints the trimmed display slice into the transcript ScrollBox and seeds
28
+ * the copy-out log from the full history so `/copy all` and `/copy <N>` can
29
+ * reach back further than what is actually rendered on resume.
30
+ */
31
+ export declare function replayResumeHistory(deps: HistoryReplayDeps, input: HistoryReplayInput): void;
32
+ export type { HistoryDisplayBlock };
33
+ //# sourceMappingURL=history-replay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history-replay.d.ts","sourceRoot":"","sources":["../../../src/tui/history-replay.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAEL,KAAK,mBAAmB,EAGzB,MAAM,cAAc,CAAC;AAGtB;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClE,qBAAqB,CAAC,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1E;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,EAAE,KAAK,EAAE,kBAAkB,GAAG,IAAI,CA8B5F;AAUD,YAAY,EAAE,mBAAmB,EAAE,CAAC"}
@@ -0,0 +1,47 @@
1
+ import { historyDisplayBlocks, limitHistoryDisplayMessages, } from "./history.js";
2
+ import { COLORS } from "./theme.js";
3
+ /**
4
+ * Paints the trimmed display slice into the transcript ScrollBox and seeds
5
+ * the copy-out log from the full history so `/copy all` and `/copy <N>` can
6
+ * reach back further than what is actually rendered on resume.
7
+ */
8
+ export function replayResumeHistory(deps, input) {
9
+ const messages = input.resumeHistoryMessages ?? Number.POSITIVE_INFINITY;
10
+ if (messages > 0 && input.history && input.history.length > 0) {
11
+ const limited = limitHistoryDisplayMessages(historyDisplayBlocks(input.history), messages);
12
+ if (limited.omittedBlocks > 0) {
13
+ deps.appendLine(`[resume] showing last ${messages} message${messages === 1 ? "" : "s"} of prior session history`, COLORS.hint);
14
+ }
15
+ for (const block of limited.blocks) {
16
+ deps.appendBlock(null, block.content, colorForHistoryBlock(block.kind));
17
+ }
18
+ }
19
+ // Seed the copy-out log from full resumed history (not the trimmed display
20
+ // slice) so `/copy all` and `/copy <N>` can reach back further than what is
21
+ // actually rendered in the transcript on resume.
22
+ if (input.history && input.history.length > 0) {
23
+ for (const block of historyDisplayBlocks(input.history)) {
24
+ if (block.kind === "user") {
25
+ // History blocks for users are formatted as `you:\n<text>`; strip the
26
+ // label so the clipboard text matches what the user originally typed.
27
+ const stripped = block.content.replace(/^you:\n?/, "");
28
+ deps.recordTranscriptEntry("user", stripped);
29
+ }
30
+ else if (block.kind === "agent") {
31
+ deps.recordTranscriptEntry("agent", block.content);
32
+ }
33
+ }
34
+ }
35
+ }
36
+ function colorForHistoryBlock(kind) {
37
+ if (kind === "user")
38
+ return COLORS.user;
39
+ if (kind === "reasoning")
40
+ return COLORS.reasoning;
41
+ if (kind === "tool")
42
+ return COLORS.tool;
43
+ if (kind === "error")
44
+ return COLORS.error;
45
+ return COLORS.agent;
46
+ }
47
+ //# sourceMappingURL=history-replay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history-replay.js","sourceRoot":"","sources":["../../../src/tui/history-replay.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,oBAAoB,EACpB,2BAA2B,GAC5B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AA0BpC;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAuB,EAAE,KAAyB;IACpF,MAAM,QAAQ,GAAG,KAAK,CAAC,qBAAqB,IAAI,MAAM,CAAC,iBAAiB,CAAC;IACzE,IAAI,QAAQ,GAAG,CAAC,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9D,MAAM,OAAO,GAAG,2BAA2B,CAAC,oBAAoB,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC3F,IAAI,OAAO,CAAC,aAAa,GAAG,CAAC,EAAE,CAAC;YAC9B,IAAI,CAAC,UAAU,CACb,yBAAyB,QAAQ,WAAW,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,2BAA2B,EAChG,MAAM,CAAC,IAAI,CACZ,CAAC;QACJ,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACnC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,oBAAoB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC;IAED,2EAA2E;IAC3E,4EAA4E;IAC5E,iDAAiD;IACjD,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9C,KAAK,MAAM,KAAK,IAAI,oBAAoB,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;YACxD,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;gBAC1B,sEAAsE;gBACtE,sEAAsE;gBACtE,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;gBACvD,IAAI,CAAC,qBAAqB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;YAC/C,CAAC;iBAAM,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBAClC,IAAI,CAAC,qBAAqB,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;YACrD,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,oBAAoB,CAAC,IAAsB;IAClD,IAAI,IAAI,KAAK,MAAM;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC;IACxC,IAAI,IAAI,KAAK,WAAW;QAAE,OAAO,MAAM,CAAC,SAAS,CAAC;IAClD,IAAI,IAAI,KAAK,MAAM;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC;IACxC,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC;IAC1C,OAAO,MAAM,CAAC,KAAK,CAAC;AACtB,CAAC"}