@oh-my-pi/pi-coding-agent 15.10.11 → 15.11.0
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.
- package/CHANGELOG.md +103 -2
- package/dist/cli.js +5790 -5731
- package/dist/types/async/index.d.ts +0 -1
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
- package/dist/types/cli-commands.d.ts +12 -0
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +18 -0
- package/dist/types/config/settings-schema.d.ts +85 -34
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
- package/dist/types/eval/py/executor.d.ts +5 -0
- package/dist/types/eval/py/kernel.d.ts +6 -1
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +3 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/history-protocol.d.ts +14 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/irc/bus.d.ts +66 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/runtime.d.ts +4 -0
- package/dist/types/memory-backend/types.d.ts +66 -1
- package/dist/types/modes/components/agent-hub.d.ts +30 -0
- package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
- package/dist/types/modes/components/custom-editor.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
- package/dist/types/modes/components/welcome.d.ts +3 -9
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/index.d.ts +3 -3
- package/dist/types/modes/interactive-mode.d.ts +10 -4
- package/dist/types/modes/oauth-manual-input.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
- package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
- package/dist/types/modes/setup-wizard/index.d.ts +5 -1
- package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +5 -2
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +51 -0
- package/dist/types/registry/agent-registry.d.ts +16 -5
- package/dist/types/secrets/index.d.ts +1 -1
- package/dist/types/secrets/obfuscator.d.ts +8 -2
- package/dist/types/session/agent-session.d.ts +49 -32
- package/dist/types/session/messages.d.ts +2 -4
- package/dist/types/session/session-history-format.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +21 -3
- package/dist/types/session/streaming-output.d.ts +46 -0
- package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
- package/dist/types/slash-commands/types.d.ts +1 -1
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +12 -2
- package/dist/types/task/index.d.ts +13 -6
- package/dist/types/task/output-manager.d.ts +0 -7
- package/dist/types/task/repair-args.d.ts +8 -7
- package/dist/types/task/types.d.ts +63 -51
- package/dist/types/thinking.d.ts +4 -0
- package/dist/types/tiny/title-client.d.ts +11 -0
- package/dist/types/tiny/title-protocol.d.ts +1 -0
- package/dist/types/tools/browser/tab-worker.d.ts +3 -1
- package/dist/types/tools/find.d.ts +0 -11
- package/dist/types/tools/grouped-file-output.d.ts +0 -49
- package/dist/types/tools/index.d.ts +7 -3
- package/dist/types/tools/irc.d.ts +76 -38
- package/dist/types/tools/job.d.ts +7 -1
- package/dist/types/utils/git.d.ts +15 -2
- package/dist/types/utils/title-generator.d.ts +3 -2
- package/examples/extensions/with-deps/package.json +1 -0
- package/package.json +11 -10
- package/scripts/bundle-dist.ts +28 -19
- package/src/async/index.ts +0 -1
- package/src/auto-thinking/classifier.ts +1 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli/gallery-fixtures/agentic.ts +230 -115
- package/src/cli/gallery-fixtures/types.ts +5 -0
- package/src/cli-commands.ts +29 -0
- package/src/cli.ts +28 -15
- package/src/commands/launch.ts +4 -0
- package/src/commit/agentic/tools/analyze-file.ts +38 -19
- package/src/commit/model-selection.ts +3 -2
- package/src/config/api-key-resolver.ts +8 -6
- package/src/config/keybindings.ts +6 -1
- package/src/config/model-registry.ts +97 -30
- package/src/config/model-resolver.ts +60 -0
- package/src/config/settings-schema.ts +99 -55
- package/src/config/settings.ts +68 -3
- package/src/edit/hashline/execute.ts +39 -2
- package/src/edit/hashline/noop-loop-guard.ts +99 -0
- package/src/eval/__tests__/agent-bridge.test.ts +5 -3
- package/src/eval/agent-bridge.ts +3 -16
- package/src/eval/completion-bridge.ts +1 -0
- package/src/eval/js/shared/prelude.txt +1 -1
- package/src/eval/py/executor.ts +29 -7
- package/src/eval/py/index.ts +6 -1
- package/src/eval/py/kernel.ts +31 -11
- package/src/eval/py/prelude.py +5 -6
- package/src/eval/py/runtime.ts +37 -0
- package/src/exec/bash-executor.ts +82 -3
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +38 -13
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/get-commands-handler.ts +2 -1
- package/src/extensibility/extensions/runner.ts +6 -1
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/shared-events.ts +2 -2
- package/src/hindsight/bank.ts +17 -2
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/internal-urls/history-protocol.ts +113 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/router.ts +3 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/irc/bus.ts +292 -0
- package/src/main.ts +26 -66
- package/src/memories/index.ts +2 -0
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/local-backend.ts +9 -0
- package/src/memory-backend/off-backend.ts +9 -0
- package/src/memory-backend/runtime.ts +66 -0
- package/src/memory-backend/types.ts +81 -1
- package/src/mnemopi/backend.ts +151 -4
- package/src/modes/acp/acp-agent.ts +119 -11
- package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
- package/src/modes/components/assistant-message.ts +19 -21
- package/src/modes/components/compaction-summary-message.ts +68 -32
- package/src/modes/components/custom-editor.ts +10 -0
- package/src/modes/components/footer.ts +3 -1
- package/src/modes/components/status-line/component.ts +118 -34
- package/src/modes/components/tool-execution.ts +31 -1
- package/src/modes/components/ttsr-notification.ts +72 -30
- package/src/modes/components/welcome.ts +9 -33
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +65 -0
- package/src/modes/controllers/extension-ui-controller.ts +8 -8
- package/src/modes/controllers/input-controller.ts +19 -2
- package/src/modes/controllers/mcp-command-controller.ts +38 -3
- package/src/modes/controllers/selector-controller.ts +21 -17
- package/src/modes/index.ts +3 -21
- package/src/modes/interactive-mode.ts +47 -22
- package/src/modes/oauth-manual-input.ts +30 -3
- package/src/modes/rpc/rpc-client.ts +154 -3
- package/src/modes/rpc/rpc-mode.ts +97 -12
- package/src/modes/rpc/rpc-subagents.ts +265 -0
- package/src/modes/rpc/rpc-types.ts +81 -1
- package/src/modes/setup-wizard/index.ts +12 -2
- package/src/modes/setup-wizard/lazy.ts +16 -0
- package/src/modes/theme/theme.ts +18 -5
- package/src/modes/types.ts +5 -5
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +51 -49
- package/src/prompts/system/irc-incoming.md +3 -4
- package/src/prompts/system/orchestrate-notice.md +2 -2
- package/src/prompts/system/subagent-system-prompt.md +0 -5
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/eval.md +3 -3
- package/src/prompts/tools/irc.md +29 -19
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/task-summary.md +5 -16
- package/src/prompts/tools/task.md +38 -29
- package/src/registry/agent-lifecycle.ts +218 -0
- package/src/registry/agent-registry.ts +16 -5
- package/src/sdk.ts +37 -10
- package/src/secrets/index.ts +8 -1
- package/src/secrets/obfuscator.ts +39 -18
- package/src/session/agent-session.ts +422 -291
- package/src/session/messages.ts +11 -78
- package/src/session/session-history-format.ts +246 -0
- package/src/session/session-manager.ts +59 -5
- package/src/session/streaming-output.ts +226 -10
- package/src/slash-commands/acp-builtins.ts +24 -0
- package/src/slash-commands/builtin-registry.ts +20 -0
- package/src/slash-commands/types.ts +1 -1
- package/src/system-prompt.ts +14 -0
- package/src/task/executor.ts +851 -461
- package/src/task/index.ts +721 -796
- package/src/task/output-manager.ts +0 -11
- package/src/task/render.ts +148 -63
- package/src/task/repair-args.ts +21 -9
- package/src/task/types.ts +82 -66
- package/src/thinking.ts +7 -0
- package/src/tiny/title-client.ts +34 -5
- package/src/tiny/title-protocol.ts +1 -1
- package/src/tiny/worker.ts +6 -4
- package/src/tools/ask.ts +4 -2
- package/src/tools/bash.ts +61 -10
- package/src/tools/browser/tab-worker.ts +26 -7
- package/src/tools/browser.ts +28 -1
- package/src/tools/find.ts +2 -27
- package/src/tools/grouped-file-output.ts +1 -118
- package/src/tools/image-gen.ts +11 -4
- package/src/tools/index.ts +17 -13
- package/src/tools/inspect-image.ts +1 -0
- package/src/tools/irc.ts +596 -171
- package/src/tools/job.ts +41 -7
- package/src/tools/read.ts +57 -1
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +4 -1
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/git.ts +267 -13
- package/src/utils/title-generator.ts +24 -5
- package/dist/types/async/support.d.ts +0 -2
- package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
- package/dist/types/task/simple-mode.d.ts +0 -8
- package/src/async/support.ts +0 -5
- package/src/task/simple-mode.ts +0 -27
|
@@ -2,16 +2,24 @@ import { Box, Container, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
|
2
2
|
import type { Rule } from "../../capability/rule";
|
|
3
3
|
import { theme } from "../../modes/theme/theme";
|
|
4
4
|
|
|
5
|
+
/** Collapsed view shows at most this many rules before eliding the rest. */
|
|
6
|
+
const MAX_COLLAPSED_RULES = 4;
|
|
7
|
+
|
|
5
8
|
/**
|
|
6
9
|
* Component that renders a TTSR (Time Traveling Stream Rules) notification.
|
|
7
10
|
* Shows when a rule violation is detected and the stream is being rewound.
|
|
11
|
+
* One block can carry several rules: a single event may match multiple rules,
|
|
12
|
+
* and consecutive notifications merge into the previous block via
|
|
13
|
+
* {@link addRules} while it is still the live transcript tail.
|
|
8
14
|
*/
|
|
9
15
|
export class TtsrNotificationComponent extends Container {
|
|
10
16
|
#box: Box;
|
|
11
17
|
#expanded = false;
|
|
18
|
+
#rules: Rule[];
|
|
12
19
|
|
|
13
|
-
constructor(
|
|
20
|
+
constructor(rules: Rule[]) {
|
|
14
21
|
super();
|
|
22
|
+
this.#rules = [...rules];
|
|
15
23
|
|
|
16
24
|
this.addChild(new Spacer(1));
|
|
17
25
|
|
|
@@ -22,6 +30,17 @@ export class TtsrNotificationComponent extends Container {
|
|
|
22
30
|
this.#rebuild();
|
|
23
31
|
}
|
|
24
32
|
|
|
33
|
+
/** Merge additional rules into this block (deduped by rule name). */
|
|
34
|
+
addRules(rules: Rule[]): void {
|
|
35
|
+
let changed = false;
|
|
36
|
+
for (const rule of rules) {
|
|
37
|
+
if (this.#rules.some(existing => existing.name === rule.name)) continue;
|
|
38
|
+
this.#rules.push(rule);
|
|
39
|
+
changed = true;
|
|
40
|
+
}
|
|
41
|
+
if (changed) this.#rebuild();
|
|
42
|
+
}
|
|
43
|
+
|
|
25
44
|
setExpanded(expanded: boolean): void {
|
|
26
45
|
if (this.#expanded !== expanded) {
|
|
27
46
|
this.#expanded = expanded;
|
|
@@ -35,46 +54,69 @@ export class TtsrNotificationComponent extends Container {
|
|
|
35
54
|
|
|
36
55
|
#rebuild(): void {
|
|
37
56
|
this.#box.clear();
|
|
57
|
+
// fg colors conflict with inverse, so styling inside the block is limited
|
|
58
|
+
// to bold (names) and italic (descriptions).
|
|
59
|
+
if (this.#rules.length === 1) {
|
|
60
|
+
this.#rebuildSingle(this.#rules[0]!);
|
|
61
|
+
} else {
|
|
62
|
+
this.#rebuildMulti();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
38
65
|
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const header = `${theme.icon.warning} Injecting ${label}: ${ruleNames}`;
|
|
66
|
+
#rebuildSingle(rule: Rule): void {
|
|
67
|
+
const header = `${theme.icon.warning} Injecting rule: ${theme.bold(rule.name)} ${theme.icon.rewind}`;
|
|
68
|
+
this.#box.addChild(new Text(header, 0, 0));
|
|
43
69
|
|
|
44
|
-
|
|
45
|
-
|
|
70
|
+
const desc = (rule.description || rule.content)?.trim();
|
|
71
|
+
if (!desc) return;
|
|
46
72
|
|
|
47
|
-
|
|
73
|
+
let displayText = desc;
|
|
74
|
+
let truncated = false;
|
|
75
|
+
if (!this.#expanded) {
|
|
76
|
+
const lines = desc.split("\n");
|
|
77
|
+
if (lines.length > 2) {
|
|
78
|
+
displayText = `${lines.slice(0, 2).join("\n")}…`;
|
|
79
|
+
truncated = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
48
82
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
83
|
+
this.#box.addChild(new Spacer(1));
|
|
84
|
+
this.#box.addChild(new Text(theme.italic(displayText), 0, 0));
|
|
85
|
+
if (truncated) {
|
|
86
|
+
this.#box.addChild(new Text(theme.italic(" (ctrl+o to expand)"), 0, 0));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#rebuildMulti(): void {
|
|
91
|
+
const header = `${theme.icon.warning} Injecting ${this.#rules.length} rules: ${theme.icon.rewind}`;
|
|
92
|
+
this.#box.addChild(new Text(header, 0, 0));
|
|
93
|
+
this.#box.addChild(new Spacer(1));
|
|
54
94
|
|
|
55
|
-
|
|
95
|
+
const visible = this.#expanded ? this.#rules : this.#rules.slice(0, MAX_COLLAPSED_RULES);
|
|
96
|
+
let elidedDetail = false;
|
|
97
|
+
for (const rule of visible) {
|
|
98
|
+
const desc = (rule.description || rule.content)?.trim();
|
|
99
|
+
let line = theme.bold(rule.name);
|
|
100
|
+
if (desc) {
|
|
101
|
+
let displayText = desc;
|
|
56
102
|
if (!this.#expanded) {
|
|
57
|
-
//
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
60
|
-
displayText = `${
|
|
103
|
+
// One line per rule when collapsed; full description when expanded.
|
|
104
|
+
const newline = desc.indexOf("\n");
|
|
105
|
+
if (newline !== -1) {
|
|
106
|
+
displayText = `${desc.slice(0, newline).trimEnd()}…`;
|
|
107
|
+
elidedDetail = true;
|
|
61
108
|
}
|
|
62
109
|
}
|
|
63
|
-
|
|
64
|
-
// Use italic for subtle distinction (fg colors conflict with inverse)
|
|
65
|
-
this.#box.addChild(new Text(theme.italic(displayText), 0, 0));
|
|
110
|
+
line += `: ${theme.italic(displayText)}`;
|
|
66
111
|
}
|
|
112
|
+
this.#box.addChild(new Text(line, 0, 0));
|
|
67
113
|
}
|
|
68
114
|
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
});
|
|
75
|
-
if (hasMoreContent) {
|
|
76
|
-
this.#box.addChild(new Text(theme.italic(" (ctrl+o to expand)"), 0, 0));
|
|
77
|
-
}
|
|
115
|
+
const hidden = this.#rules.length - visible.length;
|
|
116
|
+
if (hidden > 0) {
|
|
117
|
+
this.#box.addChild(new Text(theme.italic(`… +${hidden} more (ctrl+o to expand)`), 0, 0));
|
|
118
|
+
} else if (elidedDetail) {
|
|
119
|
+
this.#box.addChild(new Text(theme.italic(" (ctrl+o to expand)"), 0, 0));
|
|
78
120
|
}
|
|
79
121
|
}
|
|
80
122
|
}
|
|
@@ -18,14 +18,8 @@ const TIPS: readonly string[] = tipsText
|
|
|
18
18
|
.filter(line => line.length > 0);
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*/
|
|
24
|
-
const PROCESS_TIP: string | undefined = TIPS.length > 0 ? TIPS[Math.floor(Math.random() * TIPS.length)] : undefined;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Fixed number of session rows in the welcome box so its height doesn't shift
|
|
28
|
-
* between the pre-TUI splash (loading placeholder) and the loaded state.
|
|
21
|
+
* Fixed number of session rows in the welcome box so its height stays stable
|
|
22
|
+
* across recent-session updates.
|
|
29
23
|
*/
|
|
30
24
|
export const WELCOME_SESSION_SLOTS = 4;
|
|
31
25
|
|
|
@@ -76,10 +70,8 @@ export interface LspServerInfo {
|
|
|
76
70
|
export class WelcomeComponent implements Component {
|
|
77
71
|
#animStart: number | null = null;
|
|
78
72
|
#animTimer: ReturnType<typeof setInterval> | null = null;
|
|
79
|
-
/**
|
|
80
|
-
#
|
|
81
|
-
/** Per-process tip so re-renders (intro, LSP updates, splash swap) don't shuffle it. */
|
|
82
|
-
readonly #tip: string | undefined = PROCESS_TIP;
|
|
73
|
+
/** Tip chosen once per instance so re-renders (intro, LSP updates) don't shuffle it. */
|
|
74
|
+
readonly #tip: string | undefined = TIPS.length > 0 ? TIPS[Math.floor(Math.random() * TIPS.length)] : undefined;
|
|
83
75
|
// Render cache: the welcome box is the first transcript-area component, so
|
|
84
76
|
// returning a stable array reference keeps the whole frame prefix stable.
|
|
85
77
|
// Bypassed while the intro animation runs (every frame differs).
|
|
@@ -90,7 +82,7 @@ export class WelcomeComponent implements Component {
|
|
|
90
82
|
private readonly version: string,
|
|
91
83
|
private modelName: string,
|
|
92
84
|
private providerName: string,
|
|
93
|
-
private recentSessions: RecentSession[]
|
|
85
|
+
private recentSessions: RecentSession[] = [],
|
|
94
86
|
private lspServers: LspServerInfo[] = [],
|
|
95
87
|
) {}
|
|
96
88
|
|
|
@@ -99,16 +91,6 @@ export class WelcomeComponent implements Component {
|
|
|
99
91
|
this.#cachedLines = undefined;
|
|
100
92
|
}
|
|
101
93
|
|
|
102
|
-
/**
|
|
103
|
-
* Freeze the logo on the intro animation's first frame. The pre-TUI startup
|
|
104
|
-
* splash uses this so the in-TUI intro — which starts at that exact frame —
|
|
105
|
-
* picks up seamlessly from the splash's static box.
|
|
106
|
-
*/
|
|
107
|
-
holdIntroFirstFrame(): void {
|
|
108
|
-
this.#holdIntroFirstFrame = true;
|
|
109
|
-
this.invalidate();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
94
|
/**
|
|
113
95
|
* Play a one-shot intro that sweeps the gradient through every phase
|
|
114
96
|
* before settling on the resting frame. Safe to call multiple times —
|
|
@@ -116,7 +98,6 @@ export class WelcomeComponent implements Component {
|
|
|
116
98
|
*/
|
|
117
99
|
playIntro(requestRender: () => void): void {
|
|
118
100
|
this.#stopAnimation();
|
|
119
|
-
this.#holdIntroFirstFrame = false;
|
|
120
101
|
this.#animStart = performance.now();
|
|
121
102
|
requestRender();
|
|
122
103
|
this.#animTimer = setInterval(() => {
|
|
@@ -217,9 +198,7 @@ export class WelcomeComponent implements Component {
|
|
|
217
198
|
|
|
218
199
|
// Recent sessions content
|
|
219
200
|
const sessionLines: string[] = [];
|
|
220
|
-
if (this.recentSessions ===
|
|
221
|
-
sessionLines.push(` ${theme.fg("dim", "Loading…")}`);
|
|
222
|
-
} else if (this.recentSessions.length === 0) {
|
|
201
|
+
if (this.recentSessions.length === 0) {
|
|
223
202
|
sessionLines.push(` ${theme.fg("dim", "No recent sessions")}`);
|
|
224
203
|
} else {
|
|
225
204
|
// Reserve width for the bullet prefix (" • ") and the trailing " (timeAgo)"
|
|
@@ -238,7 +217,7 @@ export class WelcomeComponent implements Component {
|
|
|
238
217
|
);
|
|
239
218
|
}
|
|
240
219
|
}
|
|
241
|
-
// Pad to the fixed slot count so the box doesn't
|
|
220
|
+
// Pad to the fixed slot count so the box height doesn't depend on session count.
|
|
242
221
|
while (sessionLines.length < WELCOME_SESSION_SLOTS) {
|
|
243
222
|
sessionLines.push("");
|
|
244
223
|
}
|
|
@@ -377,9 +356,9 @@ export class WelcomeComponent implements Component {
|
|
|
377
356
|
return str + padding(width - visLen);
|
|
378
357
|
}
|
|
379
358
|
|
|
380
|
-
/** Pick the logo frame for the current intro phase, or the resting
|
|
359
|
+
/** Pick the logo frame for the current intro phase, or the resting frame. */
|
|
381
360
|
#currentLogoFrame(): readonly string[] {
|
|
382
|
-
if (this.#animStart == null) return
|
|
361
|
+
if (this.#animStart == null) return REST_FRAME;
|
|
383
362
|
const elapsed = performance.now() - this.#animStart;
|
|
384
363
|
if (elapsed >= INTRO_MS) return REST_FRAME;
|
|
385
364
|
return introLogoFrame(elapsed / INTRO_MS);
|
|
@@ -510,8 +489,5 @@ function introLogoFrame(progress: number): string[] {
|
|
|
510
489
|
return gradientLogo(PI_LOGO, phase, { strength: shineStrength, pos: shinePos });
|
|
511
490
|
}
|
|
512
491
|
|
|
513
|
-
/** First intro frame, cached for splash-held renders (resize re-renders reuse it). */
|
|
514
|
-
const INTRO_FIRST_FRAME = introLogoFrame(0);
|
|
515
|
-
|
|
516
492
|
/** Resting gradient frame, cached for re-renders outside of the intro. */
|
|
517
493
|
const REST_FRAME = gradientLogo(PI_LOGO, 0);
|
|
@@ -77,6 +77,14 @@ export class EventController {
|
|
|
77
77
|
// Insertion-ordered IRC cards not yet retired; values are the transcript
|
|
78
78
|
// components each card contributed (see #retireIrcCard for the guard).
|
|
79
79
|
#liveIrcCards = new Map<string, Component[]>();
|
|
80
|
+
// Most recent `job` tool block whose result still had every watched job
|
|
81
|
+
// running. Kept un-finalized (live) so the next `job` call displaces it —
|
|
82
|
+
// one persistent poll instead of a stack of "waiting on N jobs" frames —
|
|
83
|
+
// and sealed in place the moment anything else lands below it.
|
|
84
|
+
#displaceablePollComponent: ToolExecutionComponent | undefined = undefined;
|
|
85
|
+
// Most recent TTSR notification block. A new ttsr_triggered event merges its
|
|
86
|
+
// rules into this block while it is still the (live-region) transcript tail.
|
|
87
|
+
#lastTtsrNotification: TtsrNotificationComponent | undefined = undefined;
|
|
80
88
|
#streamingReveal: StreamingRevealController;
|
|
81
89
|
#handlers: AgentSessionEventHandlers;
|
|
82
90
|
|
|
@@ -282,6 +290,7 @@ export class EventController {
|
|
|
282
290
|
const signature = `${textContent}\u0000${imageCount}`;
|
|
283
291
|
|
|
284
292
|
this.#resetReadGroup();
|
|
293
|
+
this.#resolveDisplaceablePoll();
|
|
285
294
|
const wasOptimistic = this.ctx.optimisticUserMessageSignature === signature;
|
|
286
295
|
const wasLocallySubmitted = this.ctx.locallySubmittedUserSignatures.delete(signature) || wasOptimistic;
|
|
287
296
|
if (!wasOptimistic) {
|
|
@@ -389,6 +398,28 @@ export class EventController {
|
|
|
389
398
|
}
|
|
390
399
|
}
|
|
391
400
|
|
|
401
|
+
/**
|
|
402
|
+
* Resolve the pending displaceable poll block before the next block lands.
|
|
403
|
+
* A follow-up `job` call displaces it — the stale "waiting on N jobs" frame
|
|
404
|
+
* is removed so repeated polls read as one persistent poll — while anything
|
|
405
|
+
* else seals it in place as final history. Removal is safe only because a
|
|
406
|
+
* displaceable block never finalizes: commits stop at the first live block,
|
|
407
|
+
* so none of its rows have entered native scrollback (see
|
|
408
|
+
* ToolExecutionComponent.isDisplaceableBlock).
|
|
409
|
+
*/
|
|
410
|
+
#resolveDisplaceablePoll(nextToolName?: string): void {
|
|
411
|
+
const previous = this.#displaceablePollComponent;
|
|
412
|
+
if (!previous) return;
|
|
413
|
+
this.#displaceablePollComponent = undefined;
|
|
414
|
+
if (nextToolName === "job" && previous.isDisplaceableBlock()) {
|
|
415
|
+
this.ctx.chatContainer.removeChild(previous);
|
|
416
|
+
}
|
|
417
|
+
// Sealing stops the waiting-poll spinner and freezes the block (for a
|
|
418
|
+
// just-removed component it only clears the animation timer).
|
|
419
|
+
previous.seal();
|
|
420
|
+
this.ctx.ui.requestRender();
|
|
421
|
+
}
|
|
422
|
+
|
|
392
423
|
async #handleNotice(event: Extract<AgentSessionEvent, { type: "notice" }>): Promise<void> {
|
|
393
424
|
const message = event.source ? `${event.source}: ${event.message}` : event.message;
|
|
394
425
|
if (event.level === "error") {
|
|
@@ -444,6 +475,7 @@ export class EventController {
|
|
|
444
475
|
continue;
|
|
445
476
|
}
|
|
446
477
|
if (!readArgsTargetInternalUrl(content.arguments)) {
|
|
478
|
+
if (!this.ctx.pendingTools.has(content.id)) this.#resolveDisplaceablePoll(content.name);
|
|
447
479
|
this.#trackReadToolCall(content.id, content.arguments);
|
|
448
480
|
const component = this.ctx.pendingTools.get(content.id);
|
|
449
481
|
if (component) {
|
|
@@ -465,6 +497,7 @@ export class EventController {
|
|
|
465
497
|
? { ...content.arguments, __partialJson: content.partialJson }
|
|
466
498
|
: content.arguments;
|
|
467
499
|
if (!this.ctx.pendingTools.has(content.id)) {
|
|
500
|
+
this.#resolveDisplaceablePoll(content.name);
|
|
468
501
|
this.#resetReadGroup();
|
|
469
502
|
const tool = this.ctx.session.getToolByName(content.name);
|
|
470
503
|
const component = new ToolExecutionComponent(
|
|
@@ -561,6 +594,9 @@ export class EventController {
|
|
|
561
594
|
component.seal();
|
|
562
595
|
}
|
|
563
596
|
}
|
|
597
|
+
// These calls will never produce a result either, so the tracked
|
|
598
|
+
// waiting poll cannot be displaced anymore — freeze it in place.
|
|
599
|
+
this.#resolveDisplaceablePoll();
|
|
564
600
|
}
|
|
565
601
|
this.#lastAssistantComponent = this.ctx.streamingComponent;
|
|
566
602
|
this.#lastAssistantComponent.setUsageInfo(event.message.usage);
|
|
@@ -589,6 +625,7 @@ export class EventController {
|
|
|
589
625
|
async #handleToolExecutionStart(event: Extract<AgentSessionEvent, { type: "tool_execution_start" }>): Promise<void> {
|
|
590
626
|
this.#updateWorkingMessageFromIntent(event.intent);
|
|
591
627
|
if (!this.ctx.pendingTools.has(event.toolCallId)) {
|
|
628
|
+
this.#resolveDisplaceablePoll(event.toolName);
|
|
592
629
|
if (event.toolName === "read" && readArgsHaveTarget(event.args) && !readArgsTargetInternalUrl(event.args)) {
|
|
593
630
|
this.#trackReadToolCall(event.toolCallId, event.args);
|
|
594
631
|
const component = this.ctx.pendingTools.get(event.toolCallId);
|
|
@@ -697,6 +734,14 @@ export class EventController {
|
|
|
697
734
|
this.ctx.pendingTools.delete(event.toolCallId);
|
|
698
735
|
this.#backgroundToolCallIds.delete(event.toolCallId);
|
|
699
736
|
}
|
|
737
|
+
if (
|
|
738
|
+
event.toolName === "job" &&
|
|
739
|
+
component instanceof ToolExecutionComponent &&
|
|
740
|
+
component.isDisplaceableBlock()
|
|
741
|
+
) {
|
|
742
|
+
// Remember the waiting poll so the next `job` call can displace it.
|
|
743
|
+
this.#displaceablePollComponent = component;
|
|
744
|
+
}
|
|
700
745
|
this.ctx.ui.requestRender();
|
|
701
746
|
}
|
|
702
747
|
}
|
|
@@ -759,6 +804,9 @@ export class EventController {
|
|
|
759
804
|
this.#readToolCallArgs.clear();
|
|
760
805
|
this.#readToolCallAssistantComponents.clear();
|
|
761
806
|
this.#resetReadGroup();
|
|
807
|
+
// The turn is over: nothing else lands this turn, so the waiting poll is
|
|
808
|
+
// final history — seal it instead of letting its spinner tick while idle.
|
|
809
|
+
this.#resolveDisplaceablePoll();
|
|
762
810
|
this.#lastAssistantComponent = undefined;
|
|
763
811
|
this.ctx.ui.requestRender();
|
|
764
812
|
this.#scheduleIdleCompaction();
|
|
@@ -908,9 +956,26 @@ export class EventController {
|
|
|
908
956
|
}
|
|
909
957
|
|
|
910
958
|
async #handleTtsrTriggered(event: Extract<AgentSessionEvent, { type: "ttsr_triggered" }>): Promise<void> {
|
|
959
|
+
// Consecutive notifications (e.g. per-tool matches from one assistant
|
|
960
|
+
// message) merge into the previous block instead of stacking. Mutating an
|
|
961
|
+
// existing block is only safe while it sits inside the live region — a
|
|
962
|
+
// still-mutating block above it means none of its rows have been committed
|
|
963
|
+
// to native scrollback yet (commits are prefix-only and stop at the first
|
|
964
|
+
// live block), so the grown block still repaints.
|
|
965
|
+
const previous = this.#lastTtsrNotification;
|
|
966
|
+
if (
|
|
967
|
+
previous &&
|
|
968
|
+
this.ctx.chatContainer.children.at(-1) === previous &&
|
|
969
|
+
this.ctx.chatContainer.isWithinLiveRegion(previous)
|
|
970
|
+
) {
|
|
971
|
+
previous.addRules(event.rules);
|
|
972
|
+
this.ctx.ui.requestRender();
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
911
975
|
const component = new TtsrNotificationComponent(event.rules);
|
|
912
976
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
913
977
|
this.ctx.present(component);
|
|
978
|
+
this.#lastTtsrNotification = component;
|
|
914
979
|
}
|
|
915
980
|
|
|
916
981
|
async #handleTodoReminder(event: Extract<AgentSessionEvent, { type: "todo_reminder" }>): Promise<void> {
|
|
@@ -140,7 +140,7 @@ export class ExtensionUiController {
|
|
|
140
140
|
reload: async () => {
|
|
141
141
|
await this.ctx.session.reload();
|
|
142
142
|
this.ctx.chatContainer.clear();
|
|
143
|
-
this.ctx.renderInitialMessages(
|
|
143
|
+
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
144
144
|
await this.ctx.reloadTodos();
|
|
145
145
|
this.ctx.showStatus("Reloaded session");
|
|
146
146
|
},
|
|
@@ -197,7 +197,7 @@ export class ExtensionUiController {
|
|
|
197
197
|
|
|
198
198
|
// Update UI
|
|
199
199
|
this.ctx.chatContainer.clear();
|
|
200
|
-
this.ctx.renderInitialMessages(
|
|
200
|
+
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
201
201
|
await this.ctx.reloadTodos();
|
|
202
202
|
this.ctx.editor.setText(result.selectedText);
|
|
203
203
|
this.ctx.showStatus("Branched to new session");
|
|
@@ -212,7 +212,7 @@ export class ExtensionUiController {
|
|
|
212
212
|
|
|
213
213
|
// Update UI
|
|
214
214
|
this.ctx.chatContainer.clear();
|
|
215
|
-
this.ctx.renderInitialMessages(
|
|
215
|
+
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
216
216
|
await this.ctx.reloadTodos();
|
|
217
217
|
if (result.editorText && !this.ctx.editor.getText().trim()) {
|
|
218
218
|
this.ctx.editor.setText(result.editorText);
|
|
@@ -230,7 +230,7 @@ export class ExtensionUiController {
|
|
|
230
230
|
}
|
|
231
231
|
setSessionTerminalTitle(this.ctx.sessionManager.getSessionName(), this.ctx.sessionManager.getCwd());
|
|
232
232
|
this.ctx.chatContainer.clear();
|
|
233
|
-
this.ctx.renderInitialMessages(
|
|
233
|
+
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
234
234
|
await this.ctx.reloadTodos();
|
|
235
235
|
return { cancelled: false };
|
|
236
236
|
},
|
|
@@ -376,7 +376,7 @@ export class ExtensionUiController {
|
|
|
376
376
|
reload: async () => {
|
|
377
377
|
await this.ctx.session.reload();
|
|
378
378
|
this.ctx.chatContainer.clear();
|
|
379
|
-
this.ctx.renderInitialMessages(
|
|
379
|
+
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
380
380
|
await this.ctx.reloadTodos();
|
|
381
381
|
this.ctx.showStatus("Reloaded session");
|
|
382
382
|
},
|
|
@@ -426,7 +426,7 @@ export class ExtensionUiController {
|
|
|
426
426
|
|
|
427
427
|
// Update UI
|
|
428
428
|
this.ctx.chatContainer.clear();
|
|
429
|
-
this.ctx.renderInitialMessages(
|
|
429
|
+
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
430
430
|
await this.ctx.reloadTodos();
|
|
431
431
|
this.ctx.editor.setText(result.selectedText);
|
|
432
432
|
this.ctx.showStatus("Branched to new session");
|
|
@@ -441,7 +441,7 @@ export class ExtensionUiController {
|
|
|
441
441
|
|
|
442
442
|
// Update UI
|
|
443
443
|
this.ctx.chatContainer.clear();
|
|
444
|
-
this.ctx.renderInitialMessages(
|
|
444
|
+
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
445
445
|
await this.ctx.reloadTodos();
|
|
446
446
|
if (result.editorText && !this.ctx.editor.getText().trim()) {
|
|
447
447
|
this.ctx.editor.setText(result.editorText);
|
|
@@ -458,7 +458,7 @@ export class ExtensionUiController {
|
|
|
458
458
|
return { cancelled: true };
|
|
459
459
|
}
|
|
460
460
|
this.ctx.chatContainer.clear();
|
|
461
|
-
this.ctx.renderInitialMessages(
|
|
461
|
+
this.ctx.renderInitialMessages({ clearTerminalHistory: true });
|
|
462
462
|
await this.ctx.reloadTodos();
|
|
463
463
|
return { cancelled: false };
|
|
464
464
|
},
|
|
@@ -235,10 +235,26 @@ export class InputController {
|
|
|
235
235
|
for (const key of this.ctx.keybindings.getKeys("app.clipboard.copyLine")) {
|
|
236
236
|
this.ctx.editor.setCustomKeyHandler(key, () => this.handleCopyCurrentLine());
|
|
237
237
|
}
|
|
238
|
-
|
|
239
|
-
this.ctx.
|
|
238
|
+
const hubKeys = new Set([
|
|
239
|
+
...this.ctx.keybindings.getKeys("app.agents.hub"),
|
|
240
|
+
...this.ctx.keybindings.getKeys("app.session.observe"),
|
|
241
|
+
]);
|
|
242
|
+
for (const key of hubKeys) {
|
|
243
|
+
this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showAgentHub());
|
|
240
244
|
}
|
|
241
245
|
|
|
246
|
+
// Double-tap left arrow on an empty editor opens the agent hub — same
|
|
247
|
+
// 500ms window as the double-escape state machine above.
|
|
248
|
+
this.ctx.editor.onLeftAtStart = () => {
|
|
249
|
+
const now = Date.now();
|
|
250
|
+
if (now - this.ctx.lastLeftTapTime < 500) {
|
|
251
|
+
this.ctx.lastLeftTapTime = 0;
|
|
252
|
+
this.ctx.showAgentHub();
|
|
253
|
+
} else {
|
|
254
|
+
this.ctx.lastLeftTapTime = now;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
242
258
|
this.#setupEnhancedPaste();
|
|
243
259
|
|
|
244
260
|
this.ctx.editor.onChange = (text: string) => {
|
|
@@ -467,6 +483,7 @@ export class InputController {
|
|
|
467
483
|
this.ctx.session.sessionId,
|
|
468
484
|
this.ctx.session.model,
|
|
469
485
|
provider => this.ctx.session.agent.metadataForProvider(provider),
|
|
486
|
+
this.ctx.titleSystemPrompt,
|
|
470
487
|
)
|
|
471
488
|
.then(async title => {
|
|
472
489
|
// Re-check: a concurrent attempt for an earlier message may have
|
|
@@ -46,9 +46,14 @@ import { theme } from "../theme/theme";
|
|
|
46
46
|
import type { InteractiveModeContext } from "../types";
|
|
47
47
|
import { groupBySource, parseRemoveArgs, readScopeFlag, showCommandMessage } from "./command-controller-shared";
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
const MCP_MANUAL_INPUT_PROVIDER_ID = "mcp";
|
|
50
|
+
const MCP_MANUAL_LOGIN_TIP = "Headless? Paste the redirect URL or code with /login <value>.";
|
|
51
|
+
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string, onTimeout?: () => void): Promise<T> {
|
|
50
52
|
const { promise: timeoutPromise, reject } = Promise.withResolvers<T>();
|
|
51
|
-
const timer = setTimeout(() =>
|
|
53
|
+
const timer = setTimeout(() => {
|
|
54
|
+
onTimeout?.();
|
|
55
|
+
reject(new Error(message));
|
|
56
|
+
}, timeoutMs);
|
|
52
57
|
return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timer));
|
|
53
58
|
}
|
|
54
59
|
|
|
@@ -591,6 +596,15 @@ export class MCPCommandController {
|
|
|
591
596
|
const resolvedClientId = clientId.trim() || parsedAuthUrl.searchParams.get("client_id") || undefined;
|
|
592
597
|
const resolvedClientSecret = clientSecret.trim() || undefined;
|
|
593
598
|
|
|
599
|
+
const manualInput = this.ctx.oauthManualInput;
|
|
600
|
+
if (manualInput.hasPending()) {
|
|
601
|
+
const pendingProvider = manualInput.pendingProviderId ?? "another provider";
|
|
602
|
+
throw new Error(
|
|
603
|
+
`OAuth login already in progress for ${pendingProvider}. Complete or cancel it before starting MCP OAuth.`,
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
let manualInputClaim: { promise: Promise<string>; clear: (reason?: string) => void } | undefined;
|
|
607
|
+
const oauthTimeout = new AbortController();
|
|
594
608
|
try {
|
|
595
609
|
// Create OAuth flow
|
|
596
610
|
const flow = new MCPOAuthFlow(
|
|
@@ -620,6 +634,7 @@ export class MCPCommandController {
|
|
|
620
634
|
0,
|
|
621
635
|
),
|
|
622
636
|
);
|
|
637
|
+
block.addChild(new Text(theme.fg("muted", MCP_MANUAL_LOGIN_TIP), 1, 0));
|
|
623
638
|
block.addChild(new Spacer(1));
|
|
624
639
|
block.addChild(new Text(theme.fg("accent", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"), 1, 0));
|
|
625
640
|
// Try to open browser automatically
|
|
@@ -644,11 +659,29 @@ export class MCPCommandController {
|
|
|
644
659
|
onProgress: (message: string) => {
|
|
645
660
|
this.ctx.present([new Spacer(1), new Text(theme.fg("muted", message), 1, 0)]);
|
|
646
661
|
},
|
|
662
|
+
onManualCodeInput: () => {
|
|
663
|
+
if (manualInputClaim) return manualInputClaim.promise;
|
|
664
|
+
const pendingInput = manualInput.tryClaimInput(MCP_MANUAL_INPUT_PROVIDER_ID);
|
|
665
|
+
if (!pendingInput) {
|
|
666
|
+
const pendingProvider = manualInput.pendingProviderId ?? "another provider";
|
|
667
|
+
throw new Error(
|
|
668
|
+
`OAuth login already in progress for ${pendingProvider}. Complete or cancel it before starting MCP OAuth.`,
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
manualInputClaim = pendingInput;
|
|
672
|
+
return pendingInput.promise;
|
|
673
|
+
},
|
|
674
|
+
signal: oauthTimeout.signal,
|
|
647
675
|
},
|
|
648
676
|
);
|
|
649
677
|
|
|
650
678
|
// Execute OAuth flow with 5 minute timeout
|
|
651
|
-
const credentials = await withTimeout(
|
|
679
|
+
const credentials = await withTimeout(
|
|
680
|
+
flow.login(),
|
|
681
|
+
5 * 60 * 1000,
|
|
682
|
+
"OAuth flow timed out after 5 minutes",
|
|
683
|
+
() => oauthTimeout.abort("MCP OAuth flow timed out"),
|
|
684
|
+
);
|
|
652
685
|
|
|
653
686
|
this.ctx.present([
|
|
654
687
|
new Spacer(1),
|
|
@@ -687,6 +720,8 @@ export class MCPCommandController {
|
|
|
687
720
|
} else {
|
|
688
721
|
throw new Error(`OAuth authentication failed: ${errorMsg}`);
|
|
689
722
|
}
|
|
723
|
+
} finally {
|
|
724
|
+
manualInputClaim?.clear("Manual MCP OAuth input cleared");
|
|
690
725
|
}
|
|
691
726
|
}
|
|
692
727
|
|