@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.7
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 +54 -0
- package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-5k4dq4g6.md} +54 -0
- package/dist/cli.js +3342 -3274
- package/dist/types/capability/index.d.ts +1 -1
- package/dist/types/capability/types.d.ts +23 -1
- package/dist/types/config/settings-schema.d.ts +39 -2
- package/dist/types/cursor.d.ts +2 -1
- package/dist/types/extensibility/extensions/runner.d.ts +4 -0
- package/dist/types/extensibility/shared-events.d.ts +18 -1
- package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
- package/dist/types/internal-urls/parse.d.ts +12 -0
- package/dist/types/internal-urls/router.d.ts +6 -0
- package/dist/types/internal-urls/types.d.ts +6 -0
- package/dist/types/lsp/config.d.ts +1 -0
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +5 -0
- package/dist/types/mcp/tool-bridge.d.ts +13 -0
- package/dist/types/modes/components/custom-editor.d.ts +5 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
- package/dist/types/sdk.d.ts +3 -1
- package/dist/types/session/agent-session-types.d.ts +8 -6
- package/dist/types/session/agent-session.d.ts +28 -1
- package/dist/types/session/model-controls.d.ts +4 -1
- package/dist/types/session/session-advisors.d.ts +7 -1
- package/dist/types/session/session-tools.d.ts +44 -6
- package/dist/types/session/streaming-output.d.ts +7 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -4
- package/dist/types/session/turn-recovery.d.ts +5 -3
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/types.d.ts +3 -11
- package/dist/types/thinking.d.ts +21 -2
- package/dist/types/tools/index.d.ts +8 -3
- package/dist/types/tools/output-meta.d.ts +5 -0
- package/dist/types/tools/read.d.ts +9 -1
- package/dist/types/tools/xdev.d.ts +53 -67
- package/dist/types/utils/cpuprofile.d.ts +51 -0
- package/dist/types/utils/inspect-image-mode.d.ts +29 -0
- package/dist/types/utils/profile-tree.d.ts +47 -0
- package/dist/types/utils/sample-profile.d.ts +67 -0
- package/dist/types/utils/title-generator.d.ts +17 -16
- package/package.json +12 -12
- package/src/capability/index.ts +43 -12
- package/src/capability/mcp.ts +21 -0
- package/src/capability/types.ts +20 -1
- package/src/cli/read-cli.ts +44 -2
- package/src/config/settings-schema.ts +42 -2
- package/src/config/settings.ts +35 -0
- package/src/cursor.ts +4 -3
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
- package/src/eval/py/runner.py +16 -2
- package/src/extensibility/extensions/runner.ts +117 -5
- package/src/extensibility/extensions/types.ts +0 -1
- package/src/extensibility/extensions/wrapper.ts +74 -42
- package/src/extensibility/hooks/tool-wrapper.ts +11 -4
- package/src/extensibility/hooks/types.ts +0 -1
- package/src/extensibility/shared-events.ts +18 -1
- package/src/internal-urls/mcp-protocol.ts +17 -3
- package/src/internal-urls/parse.ts +31 -0
- package/src/internal-urls/router.ts +24 -4
- package/src/internal-urls/types.ts +6 -0
- package/src/live/transport.ts +2 -2
- package/src/lsp/client.ts +2 -2
- package/src/lsp/config.ts +4 -0
- package/src/lsp/types.ts +2 -0
- package/src/mcp/config.ts +26 -14
- package/src/mcp/manager.ts +26 -9
- package/src/mcp/tool-bridge.ts +52 -1
- package/src/memories/index.ts +25 -6
- package/src/modes/components/custom-editor.ts +39 -16
- package/src/modes/components/status-line/segments.ts +3 -1
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +8 -7
- package/src/modes/controllers/command-controller.ts +10 -10
- package/src/modes/controllers/extension-ui-controller.ts +7 -7
- package/src/modes/controllers/selector-controller.ts +7 -2
- package/src/modes/rpc/rpc-mode.ts +60 -47
- package/src/prompts/steering/user-interjection.md +2 -5
- package/src/prompts/tools/task.md +4 -2
- package/src/sdk.ts +64 -58
- package/src/session/agent-session-types.ts +8 -5
- package/src/session/agent-session.ts +144 -11
- package/src/session/model-controls.ts +48 -12
- package/src/session/session-advisors.ts +30 -14
- package/src/session/session-listing.ts +66 -4
- package/src/session/session-tools.ts +162 -54
- package/src/session/streaming-output.ts +18 -6
- package/src/session/tool-choice-queue.ts +19 -4
- package/src/session/turn-recovery.ts +25 -6
- package/src/slash-commands/builtin-registry.ts +69 -0
- package/src/task/executor.ts +11 -3
- package/src/task/index.ts +43 -32
- package/src/task/types.ts +12 -17
- package/src/thinking.ts +68 -5
- package/src/tools/bash.ts +16 -9
- package/src/tools/index.ts +36 -16
- package/src/tools/output-meta.ts +20 -0
- package/src/tools/read.ts +89 -15
- package/src/tools/write.ts +16 -7
- package/src/tools/xdev.ts +198 -210
- package/src/utils/cpuprofile.ts +235 -0
- package/src/utils/inspect-image-mode.ts +39 -0
- package/src/utils/profile-tree.ts +111 -0
- package/src/utils/sample-profile.ts +437 -0
- package/src/utils/title-generator.ts +88 -34
- package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
- package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
- package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
- package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
- package/dist/types/cli/update-cli.test.d.ts +0 -1
- package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
- package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
- package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
- package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
- package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
- package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
- package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
- package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
- package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
- package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
- package/dist/types/hindsight/client.test.d.ts +0 -1
- package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
- package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
- package/dist/types/launch/broker-list-order.test.d.ts +0 -1
- package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
- package/dist/types/launch/protocol.test.d.ts +0 -1
- package/dist/types/launch/spawn-options.test.d.ts +0 -1
- package/dist/types/launch/terminal-output.test.d.ts +0 -1
- package/dist/types/live/protocol.test.d.ts +0 -1
- package/dist/types/mcp/config-writer.test.d.ts +0 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
- package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
- package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
- package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
- package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
- package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
- package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
- package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
- package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
- package/dist/types/modes/print-mode.test.d.ts +0 -1
- package/dist/types/modes/session-teardown.test.d.ts +0 -1
- package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
- package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
- package/dist/types/modes/warp-events.test.d.ts +0 -1
- package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
- package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
- package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
- package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
- package/dist/types/session/blob-store.test.d.ts +0 -1
- package/dist/types/session/messages.test.d.ts +0 -1
- package/dist/types/session/session-context.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
- package/dist/types/system-prompt.test.d.ts +0 -1
- package/dist/types/task/render.test.d.ts +0 -1
- package/dist/types/task/spawn-policy.test.d.ts +0 -1
- package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
- package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
- package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
- package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
- package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
- package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
- package/src/advisor/__tests__/advisor.test.ts +0 -4889
- package/src/advisor/__tests__/config.test.ts +0 -349
- package/src/advisor/__tests__/emission-guard.test.ts +0 -147
- package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
- package/src/cli/update-cli.test.ts +0 -28
- package/src/config/__tests__/model-registry.test.ts +0 -182
- package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
- package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
- package/src/eval/__tests__/budget-bridge.test.ts +0 -80
- package/src/eval/__tests__/completion-bridge.test.ts +0 -412
- package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
- package/src/eval/__tests__/idle-timeout.test.ts +0 -80
- package/src/eval/__tests__/js-context-manager.test.ts +0 -456
- package/src/eval/__tests__/julia-prelude.test.ts +0 -66
- package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
- package/src/eval/__tests__/prelude-agent.test.ts +0 -156
- package/src/eval/__tests__/process-entry-import.test.ts +0 -137
- package/src/eval/py/__tests__/prelude.test.ts +0 -104
- package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
- package/src/hindsight/client.test.ts +0 -75
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
- package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
- package/src/launch/broker-list-order.test.ts +0 -89
- package/src/launch/broker-output-snapshot.test.ts +0 -126
- package/src/launch/protocol.test.ts +0 -59
- package/src/launch/spawn-options.test.ts +0 -31
- package/src/launch/terminal-output.test.ts +0 -107
- package/src/live/protocol.test.ts +0 -140
- package/src/mcp/config-writer.test.ts +0 -43
- package/src/mcp/smithery-auth.test.ts +0 -29
- package/src/mcp/smithery-registry.test.ts +0 -51
- package/src/mcp/transports/stdio.test.ts +0 -427
- package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
- package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
- package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
- package/src/modes/components/__tests__/skill-message.test.ts +0 -94
- package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
- package/src/modes/components/custom-editor.test.ts +0 -510
- package/src/modes/components/login-dialog.test.ts +0 -56
- package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
- package/src/modes/components/status-line/component.test.ts +0 -84
- package/src/modes/components/tool-execution.test.ts +0 -162
- package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
- package/src/modes/noninteractive-dispose.test.ts +0 -73
- package/src/modes/print-mode.test.ts +0 -71
- package/src/modes/session-teardown.test.ts +0 -219
- package/src/modes/theme/mermaid-rendering.test.ts +0 -53
- package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
- package/src/modes/warp-events.test.ts +0 -794
- package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
- package/src/plan-mode/model-transition.test.ts +0 -60
- package/src/plan-mode/reentry-prompt.test.ts +0 -41
- package/src/session/agent-session-error-log.test.ts +0 -59
- package/src/session/blob-store.test.ts +0 -56
- package/src/session/messages.test.ts +0 -282
- package/src/session/session-context.test.ts +0 -384
- package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
- package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
- package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
- package/src/system-prompt.test.ts +0 -236
- package/src/task/render.test.ts +0 -290
- package/src/task/spawn-policy.test.ts +0 -62
- package/src/tools/__tests__/eval-description.test.ts +0 -18
- package/src/tools/__tests__/glob.test.ts +0 -37
- package/src/tools/__tests__/json-tree.test.ts +0 -35
- package/src/tools/__tests__/vibe-render.test.ts +0 -210
- package/src/tools/hub/launch-compat.test.ts +0 -40
- package/src/vibe/__tests__/token-rate.test.ts +0 -96
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser and renderer for macOS `/usr/bin/sample` call-tree reports
|
|
3
|
+
* (conventionally saved as `*.sample.txt`).
|
|
4
|
+
*
|
|
5
|
+
* The raw report is a 10k+ line ASCII call tree with mangled symbols —
|
|
6
|
+
* expensive for an agent to digest. `renderSampleProfile` converts it into a
|
|
7
|
+
* compact bottleneck summary:
|
|
8
|
+
*
|
|
9
|
+
* - per-thread hot paths, pruned to frames with meaningful on-CPU samples,
|
|
10
|
+
* with pass-through chains collapsed and direct recursion flattened
|
|
11
|
+
* - blocked/idle threads reduced to a one-line classification
|
|
12
|
+
* - a process-wide "top functions by self samples" table
|
|
13
|
+
* - Rust v0 and legacy symbols demangled (best-effort, path extraction)
|
|
14
|
+
*
|
|
15
|
+
* Consumed by the read tool: `*.sample.txt` reads show the summary, `:raw`
|
|
16
|
+
* returns the original bytes.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { formatPct, mergeInto, type ProfileNode, type RenderTreeContext, renderProfileNode } from "./profile-tree";
|
|
20
|
+
|
|
21
|
+
/** Matches paths the read tool should treat as macOS sample reports. */
|
|
22
|
+
export function isSampleProfilePath(filePath: string): boolean {
|
|
23
|
+
return /\.sample\.txt$/i.test(filePath);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Leaf symbols that mean "thread is parked in the kernel", not burning CPU. */
|
|
27
|
+
const WAIT_SYMBOLS: Record<string, true> = {
|
|
28
|
+
__accept: true,
|
|
29
|
+
__psynch_cvwait: true,
|
|
30
|
+
__psynch_mutexwait: true,
|
|
31
|
+
__psynch_rw_rdlock: true,
|
|
32
|
+
__psynch_rw_wrlock: true,
|
|
33
|
+
__recvfrom: true,
|
|
34
|
+
__select: true,
|
|
35
|
+
__semwait_signal: true,
|
|
36
|
+
__sigwait: true,
|
|
37
|
+
__ulock_wait: true,
|
|
38
|
+
__ulock_wait2: true,
|
|
39
|
+
__wait4: true,
|
|
40
|
+
__workq_kernreturn: true,
|
|
41
|
+
kevent: true,
|
|
42
|
+
kevent64: true,
|
|
43
|
+
kevent_qos: true,
|
|
44
|
+
mach_msg2_trap: true,
|
|
45
|
+
mach_msg_trap: true,
|
|
46
|
+
poll: true,
|
|
47
|
+
semaphore_timedwait_trap: true,
|
|
48
|
+
semaphore_wait_trap: true,
|
|
49
|
+
start_wqthread: true,
|
|
50
|
+
swtch_pri: true,
|
|
51
|
+
thread_suspend: true,
|
|
52
|
+
usleep: true,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** One frame in the sampled call tree. Counts are sample hits (subtree total). */
|
|
56
|
+
export interface SampleFrame {
|
|
57
|
+
count: number;
|
|
58
|
+
symbol: string;
|
|
59
|
+
module?: string;
|
|
60
|
+
children: SampleFrame[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** One sampled thread: `Thread_<id>` root plus its call tree. */
|
|
64
|
+
export interface SampleThread {
|
|
65
|
+
id: string;
|
|
66
|
+
name?: string;
|
|
67
|
+
total: number;
|
|
68
|
+
roots: SampleFrame[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Metadata from the report preamble (everything before `Call graph:`). */
|
|
72
|
+
export interface SampleProfileHeader {
|
|
73
|
+
process: string;
|
|
74
|
+
pid: number;
|
|
75
|
+
intervalMs: number;
|
|
76
|
+
path?: string;
|
|
77
|
+
codeType?: string;
|
|
78
|
+
osVersion?: string;
|
|
79
|
+
footprint?: string;
|
|
80
|
+
footprintPeak?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Parsed macOS sample report. */
|
|
84
|
+
export interface SampleProfile {
|
|
85
|
+
header: SampleProfileHeader;
|
|
86
|
+
threads: SampleThread[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const ANALYSIS_RE = /^Analysis of sampling (.+?) \(pid (\d+)\) every (\d+) milliseconds?/;
|
|
90
|
+
const THREAD_RE = /^Thread_([^\s:]+):?\s*(.*)$/;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Parse a macOS `sample` report. Returns null when the text does not look
|
|
94
|
+
* like sample output (missing analysis preamble or `Call graph:` section).
|
|
95
|
+
*/
|
|
96
|
+
export function parseSampleProfile(text: string): SampleProfile | null {
|
|
97
|
+
const lines = text.split("\n");
|
|
98
|
+
const analysis = ANALYSIS_RE.exec(lines[0] ?? "");
|
|
99
|
+
if (!analysis) return null;
|
|
100
|
+
const callGraphIx = lines.indexOf("Call graph:");
|
|
101
|
+
if (callGraphIx === -1) return null;
|
|
102
|
+
|
|
103
|
+
const header: SampleProfileHeader = {
|
|
104
|
+
process: analysis[1],
|
|
105
|
+
pid: Number(analysis[2]),
|
|
106
|
+
intervalMs: Number(analysis[3]),
|
|
107
|
+
};
|
|
108
|
+
for (const line of lines.slice(1, callGraphIx)) {
|
|
109
|
+
const kv = /^([A-Za-z/ ()]+?):\s+(.*)$/.exec(line);
|
|
110
|
+
if (!kv) continue;
|
|
111
|
+
const value = kv[2].trim();
|
|
112
|
+
switch (kv[1]) {
|
|
113
|
+
case "Path":
|
|
114
|
+
header.path = value;
|
|
115
|
+
break;
|
|
116
|
+
case "Code Type":
|
|
117
|
+
header.codeType = value;
|
|
118
|
+
break;
|
|
119
|
+
case "OS Version":
|
|
120
|
+
header.osVersion = value;
|
|
121
|
+
break;
|
|
122
|
+
case "Physical footprint":
|
|
123
|
+
header.footprint = value;
|
|
124
|
+
break;
|
|
125
|
+
case "Physical footprint (peak)":
|
|
126
|
+
header.footprintPeak = value;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const threads: SampleThread[] = [];
|
|
132
|
+
let thread: SampleThread | undefined;
|
|
133
|
+
// Stack of (depth, frame) for the current thread; depth 1 = thread root frame.
|
|
134
|
+
const stack: Array<{ depth: number; frame: SampleFrame }> = [];
|
|
135
|
+
|
|
136
|
+
for (let ix = callGraphIx + 1; ix < lines.length; ix++) {
|
|
137
|
+
const line = lines[ix];
|
|
138
|
+
if (/^(Total number in stack|Sort by top of stack|Binary Images:)/.test(line)) break;
|
|
139
|
+
if (!line.startsWith(" ")) continue;
|
|
140
|
+
const body = line.slice(4);
|
|
141
|
+
// Decorators are one char + one space per tree level (`+ ! : | `), so the
|
|
142
|
+
// sample count starts at an even offset; scan pairs until the first digit.
|
|
143
|
+
let p = 0;
|
|
144
|
+
while (p < body.length && (body[p] < "0" || body[p] > "9")) p += 2;
|
|
145
|
+
if (p >= body.length) continue;
|
|
146
|
+
const depth = p / 2;
|
|
147
|
+
const rest = /^(\d+)\s+(.*)$/.exec(body.slice(p));
|
|
148
|
+
if (!rest) continue;
|
|
149
|
+
const count = Number(rest[1]);
|
|
150
|
+
const frameText = rest[2];
|
|
151
|
+
|
|
152
|
+
if (depth === 0) {
|
|
153
|
+
const tm = THREAD_RE.exec(frameText);
|
|
154
|
+
if (!tm) continue;
|
|
155
|
+
const name = tm[2].trim().replace(/\s+/g, " ");
|
|
156
|
+
thread = { id: tm[1], name: name || undefined, total: count, roots: [] };
|
|
157
|
+
threads.push(thread);
|
|
158
|
+
stack.length = 0;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (!thread) continue;
|
|
162
|
+
const frame: SampleFrame = { count, ...parseFrameText(frameText), children: [] };
|
|
163
|
+
while (stack.length > 0 && stack[stack.length - 1].depth >= depth) stack.pop();
|
|
164
|
+
const siblings = stack.length > 0 ? stack[stack.length - 1].frame.children : thread.roots;
|
|
165
|
+
siblings.push(frame);
|
|
166
|
+
stack.push({ depth, frame });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (threads.length === 0) return null;
|
|
170
|
+
return { header, threads };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Split `symbol (in module) + offsets [addrs]` into symbol + module. */
|
|
174
|
+
function parseFrameText(text: string): { symbol: string; module?: string } {
|
|
175
|
+
let s = text;
|
|
176
|
+
const addrIx = s.lastIndexOf(" [");
|
|
177
|
+
if (addrIx !== -1 && s.endsWith("]")) s = s.slice(0, addrIx);
|
|
178
|
+
s = s.replace(/ \+ [0-9][0-9,.]*$/, "");
|
|
179
|
+
const modIx = s.lastIndexOf(" (in ");
|
|
180
|
+
if (modIx !== -1 && s.endsWith(")")) {
|
|
181
|
+
return { symbol: s.slice(0, modIx).trim(), module: s.slice(modIx + 6, -1) };
|
|
182
|
+
}
|
|
183
|
+
return { symbol: s.trim() };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Demangling
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
const LEGACY_ESCAPES: ReadonlyArray<[string, string]> = [
|
|
191
|
+
["$LT$", "<"],
|
|
192
|
+
["$GT$", ">"],
|
|
193
|
+
["$RF$", "&"],
|
|
194
|
+
["$BP$", "*"],
|
|
195
|
+
["$C$", ","],
|
|
196
|
+
["$u20$", " "],
|
|
197
|
+
["$u27$", "'"],
|
|
198
|
+
["$u7b$", "{"],
|
|
199
|
+
["$u7d$", "}"],
|
|
200
|
+
["..", "::"],
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
const IDENT_RE = /^[A-Za-z0-9_.$]+$/;
|
|
204
|
+
const LEGACY_HASH_RE = /^h[0-9a-f]{16}$/;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Best-effort demangle of Rust symbols (v0 `_R…` and legacy `_ZN…E`).
|
|
208
|
+
* For v0 this is a path extractor, not a full demangler: identifiers are
|
|
209
|
+
* pulled out in order and joined with `::`, so generic arguments appear as
|
|
210
|
+
* extra path segments. Non-Rust symbols pass through unchanged.
|
|
211
|
+
*/
|
|
212
|
+
export function demangleSymbol(raw: string): string {
|
|
213
|
+
if (raw.startsWith("_R")) return demangleV0(raw) ?? raw;
|
|
214
|
+
const legacy = /^_?_ZN(.*)$/.exec(raw);
|
|
215
|
+
if (legacy) return demangleLegacy(legacy[1]) ?? raw;
|
|
216
|
+
return raw;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function demangleV0(raw: string): string | null {
|
|
220
|
+
const s = raw.slice(2);
|
|
221
|
+
const parts: string[] = [];
|
|
222
|
+
let i = 0;
|
|
223
|
+
while (i < s.length) {
|
|
224
|
+
const ch = s[i];
|
|
225
|
+
if (ch >= "1" && ch <= "9") {
|
|
226
|
+
let j = i;
|
|
227
|
+
while (j < s.length && s[j] >= "0" && s[j] <= "9") j++;
|
|
228
|
+
const len = Number(s.slice(i, j));
|
|
229
|
+
// v0 inserts a `_` separator when the identifier starts with a digit or `_`.
|
|
230
|
+
let k = j;
|
|
231
|
+
if (s[k] === "_") k++;
|
|
232
|
+
const ident = s.slice(k, k + len);
|
|
233
|
+
if (ident.length === len && IDENT_RE.test(ident)) {
|
|
234
|
+
parts.push(ident);
|
|
235
|
+
i = k + len;
|
|
236
|
+
} else {
|
|
237
|
+
i = j;
|
|
238
|
+
}
|
|
239
|
+
} else if (ch === "s" || ch === "B") {
|
|
240
|
+
// Disambiguator (`s<base62>_`) or backref (`B<base62>_`): skip.
|
|
241
|
+
const m = /^[sB][0-9a-zA-Z]*_/.exec(s.slice(i));
|
|
242
|
+
i += m ? m[0].length : 1;
|
|
243
|
+
} else {
|
|
244
|
+
i++;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return parts.length > 0 ? parts.join("::") : null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function demangleLegacy(s: string): string | null {
|
|
251
|
+
const parts: string[] = [];
|
|
252
|
+
let i = 0;
|
|
253
|
+
while (i < s.length && s[i] >= "0" && s[i] <= "9") {
|
|
254
|
+
let j = i;
|
|
255
|
+
while (j < s.length && s[j] >= "0" && s[j] <= "9") j++;
|
|
256
|
+
const len = Number(s.slice(i, j));
|
|
257
|
+
const ident = s.slice(j, j + len);
|
|
258
|
+
if (ident.length !== len) break;
|
|
259
|
+
if (!LEGACY_HASH_RE.test(ident)) parts.push(unescapeLegacy(ident));
|
|
260
|
+
i = j + len;
|
|
261
|
+
}
|
|
262
|
+
return parts.length > 0 ? parts.join("::") : null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function unescapeLegacy(ident: string): string {
|
|
266
|
+
let out = ident;
|
|
267
|
+
for (const [from, to] of LEGACY_ESCAPES) out = out.replaceAll(from, to);
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// Rendering
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
/** Fraction of a thread's on-CPU samples a subtree needs to stay visible. */
|
|
276
|
+
const PRUNE_FRACTION = 0.02;
|
|
277
|
+
/** Threads with fewer on-CPU samples than this share of their total are "idle". */
|
|
278
|
+
const IDLE_FRACTION = 0.002;
|
|
279
|
+
const IDLE_MIN_SAMPLES = 10;
|
|
280
|
+
const TOP_FUNCTIONS = 20;
|
|
281
|
+
|
|
282
|
+
function selfOf(frame: SampleFrame): number {
|
|
283
|
+
let children = 0;
|
|
284
|
+
for (const child of frame.children) children += child.count;
|
|
285
|
+
return Math.max(0, frame.count - children);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Build a display node for a frame subtree: demangled label, on-CPU value
|
|
290
|
+
* (self samples of non-wait frames), same-symbol siblings merged (`sample`
|
|
291
|
+
* splits them by call-site offset, which only fragments hot totals).
|
|
292
|
+
*/
|
|
293
|
+
function buildProfileNode(frame: SampleFrame, demangleCache: Map<string, string>, mainModule: string): ProfileNode {
|
|
294
|
+
let symbol = demangleCache.get(frame.symbol);
|
|
295
|
+
if (symbol === undefined) {
|
|
296
|
+
symbol = demangleSymbol(frame.symbol);
|
|
297
|
+
demangleCache.set(frame.symbol, symbol);
|
|
298
|
+
}
|
|
299
|
+
const children: ProfileNode[] = [];
|
|
300
|
+
for (const rawChild of frame.children) {
|
|
301
|
+
const child = buildProfileNode(rawChild, demangleCache, mainModule);
|
|
302
|
+
const existing = children.find(c => c.key === child.key);
|
|
303
|
+
if (existing) mergeInto(existing, child);
|
|
304
|
+
else children.push(child);
|
|
305
|
+
}
|
|
306
|
+
let cpu = WAIT_SYMBOLS[frame.symbol] ? 0 : selfOf(frame);
|
|
307
|
+
for (const child of children) cpu += child.value;
|
|
308
|
+
const label = frame.module && frame.module !== mainModule ? `${symbol} (${frame.module})` : symbol;
|
|
309
|
+
return { key: symbol, label, value: cpu, recursion: 0, children };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Aggregate on-CPU self samples per demangled symbol across all threads. */
|
|
313
|
+
function aggregateSelf(
|
|
314
|
+
profile: SampleProfile,
|
|
315
|
+
demangleCache: Map<string, string>,
|
|
316
|
+
): Map<string, { cpu: number; module?: string }> {
|
|
317
|
+
const totals = new Map<string, { cpu: number; module?: string }>();
|
|
318
|
+
const visit = (frame: SampleFrame): void => {
|
|
319
|
+
if (!WAIT_SYMBOLS[frame.symbol]) {
|
|
320
|
+
const self = selfOf(frame);
|
|
321
|
+
if (self > 0) {
|
|
322
|
+
let symbol = demangleCache.get(frame.symbol);
|
|
323
|
+
if (symbol === undefined) {
|
|
324
|
+
symbol = demangleSymbol(frame.symbol);
|
|
325
|
+
demangleCache.set(frame.symbol, symbol);
|
|
326
|
+
}
|
|
327
|
+
const entry = totals.get(symbol);
|
|
328
|
+
if (entry) entry.cpu += self;
|
|
329
|
+
else totals.set(symbol, { cpu: self, module: frame.module });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
for (const child of frame.children) visit(child);
|
|
333
|
+
};
|
|
334
|
+
for (const thread of profile.threads) for (const root of thread.roots) visit(root);
|
|
335
|
+
return totals;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Dominant wait leaf of an idle thread ("what is it blocked on"). */
|
|
339
|
+
function dominantWait(thread: SampleThread): string | undefined {
|
|
340
|
+
let best: { symbol: string; count: number } | undefined;
|
|
341
|
+
const visit = (frame: SampleFrame): void => {
|
|
342
|
+
if (frame.children.length === 0 && WAIT_SYMBOLS[frame.symbol]) {
|
|
343
|
+
if (!best || frame.count > best.count) best = { symbol: frame.symbol, count: frame.count };
|
|
344
|
+
}
|
|
345
|
+
for (const child of frame.children) visit(child);
|
|
346
|
+
};
|
|
347
|
+
for (const root of thread.roots) visit(root);
|
|
348
|
+
return best?.symbol;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Render a macOS sample report as an agent-friendly bottleneck summary.
|
|
353
|
+
* Returns null when `text` is not sample output (caller falls back to the
|
|
354
|
+
* plain-text path).
|
|
355
|
+
*/
|
|
356
|
+
export function renderSampleProfile(text: string): string | null {
|
|
357
|
+
const profile = parseSampleProfile(text);
|
|
358
|
+
if (!profile) return null;
|
|
359
|
+
const { header, threads } = profile;
|
|
360
|
+
const demangleCache = new Map<string, string>();
|
|
361
|
+
|
|
362
|
+
const annotated = threads.map(thread => {
|
|
363
|
+
const roots = thread.roots.map(root => buildProfileNode(root, demangleCache, header.process));
|
|
364
|
+
let cpu = 0;
|
|
365
|
+
for (const root of roots) cpu += root.value;
|
|
366
|
+
return { thread, roots, cpu };
|
|
367
|
+
});
|
|
368
|
+
const processCpu = annotated.reduce((sum, t) => sum + t.cpu, 0);
|
|
369
|
+
const maxSamples = threads.reduce((max, t) => Math.max(max, t.total), 0);
|
|
370
|
+
|
|
371
|
+
const out: string[] = [];
|
|
372
|
+
out.push(`macOS sample profile: ${header.process} (pid ${header.pid}), sampled every ${header.intervalMs} ms`);
|
|
373
|
+
const meta: string[] = [];
|
|
374
|
+
if (header.path) meta.push(header.path);
|
|
375
|
+
if (header.codeType) meta.push(header.codeType);
|
|
376
|
+
if (header.osVersion) meta.push(`macOS ${header.osVersion.replace(/^macOS\s+/, "")}`);
|
|
377
|
+
if (meta.length > 0) out.push(meta.join(" | "));
|
|
378
|
+
const durationSec = (maxSamples * header.intervalMs) / 1000;
|
|
379
|
+
let statLine = `Duration: ~${durationSec.toFixed(1)} s (${maxSamples} samples/thread)`;
|
|
380
|
+
if (header.footprint) {
|
|
381
|
+
statLine += ` | Footprint: ${header.footprint}${header.footprintPeak ? ` (peak ${header.footprintPeak})` : ""}`;
|
|
382
|
+
}
|
|
383
|
+
out.push(statLine);
|
|
384
|
+
out.push("");
|
|
385
|
+
out.push(
|
|
386
|
+
`Process total: ${processCpu} on-CPU samples across ${threads.length} threads. Counts and percentages below are on-CPU samples (blocked time excluded).`,
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const active: typeof annotated = [];
|
|
390
|
+
const idle: typeof annotated = [];
|
|
391
|
+
for (const entry of annotated) {
|
|
392
|
+
const threshold = Math.max(IDLE_MIN_SAMPLES, entry.thread.total * IDLE_FRACTION);
|
|
393
|
+
(entry.cpu >= threshold ? active : idle).push(entry);
|
|
394
|
+
}
|
|
395
|
+
active.sort((a, b) => b.cpu - a.cpu);
|
|
396
|
+
|
|
397
|
+
for (const { thread, roots, cpu } of active) {
|
|
398
|
+
out.push("");
|
|
399
|
+
const title = thread.name ? `${thread.name} (Thread_${thread.id})` : `Thread_${thread.id}`;
|
|
400
|
+
out.push(`## ${title} — ${thread.total} samples, ${cpu} on-CPU (${formatPct(cpu, thread.total)})`);
|
|
401
|
+
const ctx: RenderTreeContext = {
|
|
402
|
+
out,
|
|
403
|
+
total: cpu,
|
|
404
|
+
minValue: Math.max(3, Math.round(cpu * PRUNE_FRACTION)),
|
|
405
|
+
formatValue: String,
|
|
406
|
+
valueWidth: 6,
|
|
407
|
+
};
|
|
408
|
+
const kept = roots.filter(root => root.value >= ctx.minValue).sort((a, b) => b.value - a.value);
|
|
409
|
+
for (const root of kept) renderProfileNode(root, 0, ctx);
|
|
410
|
+
if (kept.length === 0) out.push(` (no call path above ${ctx.minValue} on-CPU samples)`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (idle.length > 0) {
|
|
414
|
+
out.push("");
|
|
415
|
+
out.push(`## Idle / negligible threads (${idle.length})`);
|
|
416
|
+
for (const { thread, cpu } of idle) {
|
|
417
|
+
const title = thread.name ? `${thread.name} (Thread_${thread.id})` : `Thread_${thread.id}`;
|
|
418
|
+
const wait = dominantWait(thread);
|
|
419
|
+
const state = wait ? `blocked in ${wait} (${cpu} on-CPU)` : `${cpu}/${thread.total} samples on-CPU`;
|
|
420
|
+
out.push(`- ${title}: ${state}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const totals = [...aggregateSelf(profile, demangleCache).entries()].sort((a, b) => b[1].cpu - a[1].cpu);
|
|
425
|
+
if (totals.length > 0) {
|
|
426
|
+
out.push("");
|
|
427
|
+
out.push("## Top functions by self samples (process-wide, blocked time excluded)");
|
|
428
|
+
for (const [symbol, { cpu, module }] of totals.slice(0, TOP_FUNCTIONS)) {
|
|
429
|
+
const suffix = module && module !== header.process ? ` (${module})` : "";
|
|
430
|
+
out.push(`${String(cpu).padStart(6)} ${formatPct(cpu, processCpu).padStart(6)} ${symbol}${suffix}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
out.push("");
|
|
435
|
+
out.push("[Summarized view of a macOS `sample` call-tree report. Use ':raw' to read the original file.]");
|
|
436
|
+
return out.join("\n");
|
|
437
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generate session titles using a smol, fast model.
|
|
3
3
|
*/
|
|
4
|
+
import { dlopen, FFIType, ptr } from "bun:ffi";
|
|
4
5
|
import * as path from "node:path";
|
|
5
6
|
|
|
6
7
|
import { type Api, type AssistantMessage, completeSimple, type Model } from "@oh-my-pi/pi-ai";
|
|
@@ -23,6 +24,59 @@ const TITLE_MARKER_INSTRUCTION = prompt.render(titleMarkerInstruction);
|
|
|
23
24
|
const DEFAULT_TERMINAL_TITLE = "π";
|
|
24
25
|
const TERMINAL_TITLE_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
25
26
|
|
|
27
|
+
interface WindowsConsoleTitleApi {
|
|
28
|
+
set(title: string): boolean;
|
|
29
|
+
close(): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let windowsConsoleTitleApi: WindowsConsoleTitleApi | null | undefined;
|
|
33
|
+
let lastTerminalTitle: string | undefined;
|
|
34
|
+
|
|
35
|
+
function getWindowsConsoleTitleApi(): WindowsConsoleTitleApi | null {
|
|
36
|
+
if (process.platform !== "win32") return null;
|
|
37
|
+
if (windowsConsoleTitleApi !== undefined) return windowsConsoleTitleApi;
|
|
38
|
+
try {
|
|
39
|
+
const kernel32 = dlopen("kernel32.dll", {
|
|
40
|
+
SetConsoleTitleW: { args: [FFIType.ptr], returns: FFIType.bool },
|
|
41
|
+
});
|
|
42
|
+
windowsConsoleTitleApi = {
|
|
43
|
+
set(title) {
|
|
44
|
+
const wideTitle = Buffer.from(`${title}\0`, "utf16le");
|
|
45
|
+
return kernel32.symbols.SetConsoleTitleW(ptr(wideTitle));
|
|
46
|
+
},
|
|
47
|
+
close: () => kernel32.close(),
|
|
48
|
+
};
|
|
49
|
+
} catch {
|
|
50
|
+
windowsConsoleTitleApi = null;
|
|
51
|
+
}
|
|
52
|
+
return windowsConsoleTitleApi;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function setWindowsConsoleTitle(title: string): boolean {
|
|
56
|
+
const api = getWindowsConsoleTitleApi();
|
|
57
|
+
if (!api) return false;
|
|
58
|
+
try {
|
|
59
|
+
return api.set(title);
|
|
60
|
+
} catch {
|
|
61
|
+
try {
|
|
62
|
+
api.close();
|
|
63
|
+
} catch {
|
|
64
|
+
// Ignore cleanup failures after the native title path has already failed.
|
|
65
|
+
}
|
|
66
|
+
windowsConsoleTitleApi = null;
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function disposeWindowsConsoleTitleApi(): void {
|
|
72
|
+
try {
|
|
73
|
+
windowsConsoleTitleApi?.close();
|
|
74
|
+
} catch {
|
|
75
|
+
// Terminal teardown must remain best-effort.
|
|
76
|
+
}
|
|
77
|
+
windowsConsoleTitleApi = undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
26
80
|
// Cover the "backend ignores `disableReasoning`" case unconditionally: the
|
|
27
81
|
// static `model.reasoning` catalog flag can't distinguish a thinking model that
|
|
28
82
|
// was declared with `reasoning: false` (e.g. Qwen3 served locally via llama.cpp,
|
|
@@ -379,11 +433,16 @@ export function formatSessionTerminalTitle(sessionName: string | undefined, cwd?
|
|
|
379
433
|
}
|
|
380
434
|
|
|
381
435
|
/**
|
|
382
|
-
* Set the terminal title
|
|
436
|
+
* Set the terminal title through the native Win32 API or OSC 0.
|
|
437
|
+
*
|
|
438
|
+
* Repeating the same sanitized title is a no-op on every platform.
|
|
383
439
|
*/
|
|
384
440
|
export function setTerminalTitle(title: string): void {
|
|
385
441
|
if (!process.stdout.isTTY || isTerminalHeadless()) return;
|
|
386
|
-
|
|
442
|
+
const next = sanitizeTerminalTitlePart(title) ?? DEFAULT_TERMINAL_TITLE;
|
|
443
|
+
if (next === lastTerminalTitle) return;
|
|
444
|
+
if (!setWindowsConsoleTitle(next)) process.stdout.write(`\x1b]0;${next}\x07`);
|
|
445
|
+
lastTerminalTitle = next;
|
|
387
446
|
}
|
|
388
447
|
|
|
389
448
|
export function setSessionTerminalTitle(sessionName: string | undefined, cwd?: string): void {
|
|
@@ -396,9 +455,9 @@ export function setSessionTerminalTitle(sessionName: string | undefined, cwd?: s
|
|
|
396
455
|
|
|
397
456
|
/**
|
|
398
457
|
* Set a terminal title from an extension's `setTitle()`. Unlike the session base
|
|
399
|
-
* title, this owns the terminal verbatim:
|
|
400
|
-
* it
|
|
401
|
-
*
|
|
458
|
+
* title, this owns the terminal verbatim: periodic and run-state updates will not
|
|
459
|
+
* rewrite it. Cleared when the app next sets an authoritative session title via
|
|
460
|
+
* {@link setSessionTerminalTitle}.
|
|
402
461
|
*/
|
|
403
462
|
export function setExtensionTerminalTitle(title: string): void {
|
|
404
463
|
terminalTitleRuntime.extensionOverride = title;
|
|
@@ -407,12 +466,9 @@ export function setExtensionTerminalTitle(title: string): void {
|
|
|
407
466
|
|
|
408
467
|
export type TerminalTitleState = "idle" | "working" | "attention";
|
|
409
468
|
|
|
410
|
-
/**
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
* set) to avoid a utils→modes import cycle; OSC titles render in tab/window bars
|
|
414
|
-
* that handle Unicode. */
|
|
415
|
-
const TITLE_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
|
|
469
|
+
/** Windows uses a static working separator instead of scheduling title animation. */
|
|
470
|
+
const WINDOWS_TITLE_WORKING_SEPARATOR = ":";
|
|
471
|
+
const TITLE_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
416
472
|
const TITLE_SPINNER_INTERVAL_MS = 80;
|
|
417
473
|
/** The user's turn: the title reads like a shell prompt awaiting input. */
|
|
418
474
|
const TITLE_IDLE_SEPARATOR = ">";
|
|
@@ -425,7 +481,6 @@ const terminalTitleRuntime: {
|
|
|
425
481
|
frame: number;
|
|
426
482
|
enabled: boolean;
|
|
427
483
|
timer: NodeJS.Timeout | undefined;
|
|
428
|
-
lastEmitted: string | undefined;
|
|
429
484
|
/** A title an extension set via `setTitle()`. While set, it owns the terminal
|
|
430
485
|
* title verbatim: the run-state separator never rewrites it. Cleared when the
|
|
431
486
|
* app next establishes an authoritative session title (rename, new session,
|
|
@@ -437,19 +492,16 @@ const terminalTitleRuntime: {
|
|
|
437
492
|
frame: 0,
|
|
438
493
|
enabled: true,
|
|
439
494
|
timer: undefined,
|
|
440
|
-
lastEmitted: undefined,
|
|
441
495
|
extensionOverride: undefined,
|
|
442
496
|
};
|
|
443
497
|
|
|
444
498
|
/**
|
|
445
|
-
* Compose the terminal title from the `π` brand, a state-carrying
|
|
446
|
-
* the session label.
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
* - `
|
|
450
|
-
* -
|
|
451
|
-
* - `attention`: `π ! label` — agent blocked on the user;
|
|
452
|
-
* - disabled: `π: label` — the pre-state layout.
|
|
499
|
+
* Compose the terminal title from the `π` brand, a state-carrying separator, and
|
|
500
|
+
* the session label. Pure (no I/O) so the state→separator contract is testable:
|
|
501
|
+
* - `idle` (user's turn): `π > label`;
|
|
502
|
+
* - `working`: `π ⠋ label` (`π : label` on Windows);
|
|
503
|
+
* - `attention`: `π ! label`;
|
|
504
|
+
* - disabled: `π: label`.
|
|
453
505
|
* Without a label the separator trails the brand (`π >`) so the state stays visible.
|
|
454
506
|
*/
|
|
455
507
|
export function buildTerminalTitleWithState(
|
|
@@ -457,11 +509,14 @@ export function buildTerminalTitleWithState(
|
|
|
457
509
|
state: TerminalTitleState,
|
|
458
510
|
frame: number,
|
|
459
511
|
enabled: boolean,
|
|
512
|
+
platform: NodeJS.Platform = process.platform,
|
|
460
513
|
): string {
|
|
461
514
|
if (!enabled) return label ? `${DEFAULT_TERMINAL_TITLE}: ${label}` : DEFAULT_TERMINAL_TITLE;
|
|
462
515
|
const separator =
|
|
463
516
|
state === "working"
|
|
464
|
-
?
|
|
517
|
+
? platform === "win32"
|
|
518
|
+
? WINDOWS_TITLE_WORKING_SEPARATOR
|
|
519
|
+
: TITLE_SPINNER_FRAMES[frame % TITLE_SPINNER_FRAMES.length]
|
|
465
520
|
: state === "attention"
|
|
466
521
|
? TITLE_ATTENTION_SEPARATOR
|
|
467
522
|
: TITLE_IDLE_SEPARATOR;
|
|
@@ -469,8 +524,8 @@ export function buildTerminalTitleWithState(
|
|
|
469
524
|
}
|
|
470
525
|
|
|
471
526
|
function emitTerminalTitle(): void {
|
|
472
|
-
// An extension override owns the terminal verbatim; the
|
|
473
|
-
//
|
|
527
|
+
// An extension override owns the terminal verbatim; the terminal sink
|
|
528
|
+
// deduplicates repeated state updates.
|
|
474
529
|
const next =
|
|
475
530
|
terminalTitleRuntime.extensionOverride ??
|
|
476
531
|
buildTerminalTitleWithState(
|
|
@@ -479,20 +534,16 @@ function emitTerminalTitle(): void {
|
|
|
479
534
|
terminalTitleRuntime.frame,
|
|
480
535
|
terminalTitleRuntime.enabled,
|
|
481
536
|
);
|
|
482
|
-
if (next === terminalTitleRuntime.lastEmitted) return;
|
|
483
|
-
terminalTitleRuntime.lastEmitted = next;
|
|
484
537
|
setTerminalTitle(next);
|
|
485
538
|
}
|
|
486
539
|
|
|
487
540
|
function stopTerminalTitleSpinner(): void {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
terminalTitleRuntime.timer = undefined;
|
|
491
|
-
}
|
|
541
|
+
clearInterval(terminalTitleRuntime.timer);
|
|
542
|
+
terminalTitleRuntime.timer = undefined;
|
|
492
543
|
}
|
|
493
544
|
|
|
494
545
|
function startTerminalTitleSpinner(): void {
|
|
495
|
-
if (terminalTitleRuntime.timer || !process.stdout.isTTY) return;
|
|
546
|
+
if (process.platform === "win32" || terminalTitleRuntime.timer || !process.stdout.isTTY) return;
|
|
496
547
|
terminalTitleRuntime.timer = setInterval(() => {
|
|
497
548
|
terminalTitleRuntime.frame = (terminalTitleRuntime.frame + 1) % TITLE_SPINNER_FRAMES.length;
|
|
498
549
|
emitTerminalTitle();
|
|
@@ -503,8 +554,9 @@ function startTerminalTitleSpinner(): void {
|
|
|
503
554
|
|
|
504
555
|
/**
|
|
505
556
|
* Reflect the agent run state in the terminal title's separator: `working`
|
|
506
|
-
* animates
|
|
507
|
-
* `attention` shows `!` (agent blocked on you). Gated off by
|
|
557
|
+
* animates outside Windows and stays `:` on Windows, `idle` shows `>` (your
|
|
558
|
+
* turn), and `attention` shows `!` (agent blocked on you). Gated off by
|
|
559
|
+
* `tui.titleState`.
|
|
508
560
|
*/
|
|
509
561
|
export function setTerminalTitleState(state: TerminalTitleState): void {
|
|
510
562
|
terminalTitleRuntime.state = state;
|
|
@@ -521,9 +573,11 @@ export function setTerminalTitleStateEnabled(enabled: boolean): void {
|
|
|
521
573
|
emitTerminalTitle();
|
|
522
574
|
}
|
|
523
575
|
|
|
524
|
-
/**
|
|
576
|
+
/** Release terminal-title runtime resources. */
|
|
525
577
|
export function disposeTerminalTitleState(): void {
|
|
526
578
|
stopTerminalTitleSpinner();
|
|
579
|
+
disposeWindowsConsoleTitleApi();
|
|
580
|
+
lastTerminalTitle = undefined;
|
|
527
581
|
}
|
|
528
582
|
|
|
529
583
|
/**
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|