@oh-my-pi/pi-tui 18.3.0 → 18.3.2
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 +24 -0
- package/THIRD-PARTY-NOTICES.txt +2 -2
- package/dist/types/chat/read-tool-group.d.ts +3 -3
- package/dist/types/chat/user-message.d.ts +4 -1
- package/dist/types/components/editor.d.ts +0 -2
- package/dist/types/components/input.d.ts +16 -0
- package/dist/types/overlays/btw-history-panel.d.ts +4 -1
- package/dist/types/overlays/rewind-selector.d.ts +4 -2
- package/dist/types/overlays/settings-defs.d.ts +5 -0
- package/dist/types/prompt/composer.d.ts +12 -1
- package/dist/types/prompt/custom-editor.d.ts +5 -26
- package/dist/types/space-hold.d.ts +45 -0
- package/dist/types/status-line/host.d.ts +2 -0
- package/dist/types/tools/cfg-render.d.ts +37 -0
- package/dist/types/tools/cfg-url.d.ts +16 -0
- package/dist/types/tools/find.d.ts +2 -2
- package/dist/types/tools/output-meta.d.ts +2 -0
- package/dist/types/tools/proc-render.d.ts +1 -8
- package/dist/types/tools/read.d.ts +14 -3
- package/dist/types/tools/result-card.d.ts +20 -0
- package/dist/types/tools/url-scheme-host.d.ts +29 -0
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/tools/xdev.d.ts +9 -1
- package/package.json +9 -9
- package/src/chat/chat-transcript-builder.ts +5 -1
- package/src/chat/read-tool-group.ts +8 -9
- package/src/chat/tool-execution.ts +7 -5
- package/src/chat/user-message.ts +16 -6
- package/src/components/editor.ts +2 -4
- package/src/components/input.ts +103 -30
- package/src/components/markdown.ts +18 -6
- package/src/overlays/annotation-overlay.ts +0 -1
- package/src/overlays/btw-history-panel.ts +4 -0
- package/src/overlays/model-hub.ts +18 -6
- package/src/overlays/rewind-selector.ts +186 -12
- package/src/overlays/settings-defs.ts +6 -0
- package/src/overlays/settings-selector.ts +4 -3
- package/src/prompt/composer.ts +83 -56
- package/src/prompt/custom-editor.ts +15 -137
- package/src/prompt/tips.txt +1 -0
- package/src/space-hold.ts +143 -0
- package/src/status-line/component.ts +6 -0
- package/src/status-line/host.ts +2 -0
- package/src/status-line/segments.ts +4 -0
- package/src/tools/cfg-render.ts +117 -0
- package/src/tools/cfg-url.ts +30 -0
- package/src/tools/find.ts +7 -6
- package/src/tools/output-meta.ts +2 -0
- package/src/tools/proc-render.ts +2 -26
- package/src/tools/read.ts +109 -68
- package/src/tools/result-card.ts +36 -0
- package/src/tools/url-scheme-host.ts +42 -0
- package/src/tools/write.ts +109 -66
- package/src/tools/xdev.ts +9 -1
- package/dist/types/chat/read-target.d.ts +0 -4
- package/src/chat/read-target.ts +0 -7
package/src/prompt/tips.txt
CHANGED
|
@@ -25,4 +25,5 @@ No native tool_calling? Inference provider botches parsing them? `PI_DIALECT=glm
|
|
|
25
25
|
Turn on `/advisor` to attach a second model that reviews every turn and quietly injects advice
|
|
26
26
|
Try starting your prompt with a ->, and writing a list (1. Do X, 2. Do Y)
|
|
27
27
|
Press shift+tab to cycle through reasoning effort levels
|
|
28
|
+
omp can reconfigure and extend itself — ask any questions you have!
|
|
28
29
|
Lint/type errors piling up? `omp cleanse` (or /cleanse right here) hunts project diagnostics and fixes them with parallel subagents — esc cancels
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/** Max gap (ms) between two spaces for the later one to count as OS key auto-repeat rather than a
|
|
2
|
+
* deliberate press. OS auto-repeat is fast; a deliberate tap (even a fast one) is slower. */
|
|
3
|
+
export const SPACE_REPEAT_MAX_GAP_MS = 120;
|
|
4
|
+
/** Two consecutive inter-space gaps are "mechanical" (machine-driven auto-repeat) when both are
|
|
5
|
+
* within {@link SPACE_REPEAT_MAX_GAP_MS} and differ by no more than this — an absolute jitter floor
|
|
6
|
+
* or, for slower repeat rates, {@link SPACE_REPEAT_JITTER_RATIO} of the smaller gap. OS key-repeat
|
|
7
|
+
* is metronomic; a human smashing the bar is fast but irregular, so its deltas never stay this
|
|
8
|
+
* steady. */
|
|
9
|
+
export const SPACE_REPEAT_JITTER_MS = 18;
|
|
10
|
+
export const SPACE_REPEAT_JITTER_RATIO = 0.35;
|
|
11
|
+
/** Consecutive mechanical (fast + steady) deltas that confirm the space bar is held and start
|
|
12
|
+
* recording. Needs a sustained metronomic cadence, so jittery smashing and deliberate taps never
|
|
13
|
+
* reach it. */
|
|
14
|
+
export const SPACE_HOLD_MECHANICAL_RUN = 2;
|
|
15
|
+
/** Idle gap (ms) after the last repeated space that counts as the space bar being released, ending
|
|
16
|
+
* the push-to-talk recording. Must comfortably exceed the OS key-repeat interval. */
|
|
17
|
+
export const SPACE_HOLD_RELEASE_MS = 250;
|
|
18
|
+
|
|
19
|
+
/** Whether two consecutive inter-space gaps look machine-driven: both within the auto-repeat band
|
|
20
|
+
* and steady enough (small absolute or proportional difference). OS key-repeat is metronomic, so
|
|
21
|
+
* its successive deltas match closely; human smashing is fast but irregular and deliberate taps are
|
|
22
|
+
* too slow, so neither passes. */
|
|
23
|
+
function gapsAreMechanical(gap: number, prevGap: number): boolean {
|
|
24
|
+
if (gap > SPACE_REPEAT_MAX_GAP_MS || prevGap > SPACE_REPEAT_MAX_GAP_MS) return false;
|
|
25
|
+
const tolerance = Math.max(SPACE_REPEAT_JITTER_MS, Math.min(gap, prevGap) * SPACE_REPEAT_JITTER_RATIO);
|
|
26
|
+
return Math.abs(gap - prevGap) <= tolerance;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** What a recognized space-bar hold drives — the push-to-talk start and stop. */
|
|
30
|
+
export interface SpaceHoldHandler {
|
|
31
|
+
/** Gate for the gesture. Returns false to keep the space bar inserting spaces normally, so
|
|
32
|
+
* disabling push-to-talk restores plain space behavior. */
|
|
33
|
+
enabled(): boolean;
|
|
34
|
+
/** A sustained hold was recognized. The optimistically-typed spaces have already been deleted. */
|
|
35
|
+
onStart(): void;
|
|
36
|
+
/** The held space bar was released (an idle gap with no further repeated spaces, or any other key). */
|
|
37
|
+
onEnd(): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** What the host must do with a keypress after {@link SpaceHoldGesture.process} has seen it:
|
|
41
|
+
* `"pass"` — handle it normally; `"type"` — insert the space now (it is tracked back out if the run
|
|
42
|
+
* turns into a hold); `"swallow"` — drop it, the gesture consumed it. */
|
|
43
|
+
export type SpaceHoldStep = "pass" | "type" | "swallow";
|
|
44
|
+
|
|
45
|
+
/** Space-hold push-to-talk state machine. A held space bar emits OS auto-repeat: a *steady* stream
|
|
46
|
+
* of spaces at a fixed fast interval. We watch the inter-space deltas and only recognize a hold once
|
|
47
|
+
* {@link SPACE_HOLD_MECHANICAL_RUN} consecutive deltas are "mechanical" — both auto-repeat-fast and
|
|
48
|
+
* near-identical (see {@link gapsAreMechanical}). Smashing the bar is fast but jittery and deliberate
|
|
49
|
+
* taps are too slow, so neither escalates and both keep typing real spaces; the few spaces typed
|
|
50
|
+
* before a real hold is recognized are tracked back out of the host input. */
|
|
51
|
+
export class SpaceHoldGesture {
|
|
52
|
+
/** Unset (the default) keeps the space bar typing spaces. */
|
|
53
|
+
handler: SpaceHoldHandler | undefined;
|
|
54
|
+
/** Deletes the given number of characters before the host input's cursor. */
|
|
55
|
+
readonly #deleteTyped: (count: number) => void;
|
|
56
|
+
/** Host input state that rules the gesture out, e.g. an editor mode where space is not text. */
|
|
57
|
+
readonly #allowed: () => boolean;
|
|
58
|
+
/** Spaces the host typed in the current run; tracked back out when a hold is recognized. */
|
|
59
|
+
#typed = 0;
|
|
60
|
+
/** Consecutive "mechanical" deltas (fast + steady); a sustained run of these confirms a held bar. */
|
|
61
|
+
#mechanicalRun = 0;
|
|
62
|
+
/** Inter-space gap (ms) of the previous space pair, compared against the next to judge steadiness. */
|
|
63
|
+
#prevGap: number | undefined;
|
|
64
|
+
/** Monotonic timestamp (ms) of the last space, to measure the gap to the next one. */
|
|
65
|
+
#lastSpaceAt = Number.NEGATIVE_INFINITY;
|
|
66
|
+
/** True while a recognized space-hold push-to-talk recording is in progress. */
|
|
67
|
+
#active = false;
|
|
68
|
+
/** Idle timer that ends the hold once repeated spaces stop arriving. */
|
|
69
|
+
#releaseTimer: NodeJS.Timeout | undefined;
|
|
70
|
+
|
|
71
|
+
constructor(deleteTyped: (count: number) => void, allowed: () => boolean = () => true) {
|
|
72
|
+
this.#deleteTyped = deleteTyped;
|
|
73
|
+
this.#allowed = allowed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Feed one keypress, `isSpace` when it is a plain space bar press. */
|
|
77
|
+
process(isSpace: boolean): SpaceHoldStep {
|
|
78
|
+
if (this.#active) {
|
|
79
|
+
if (isSpace) {
|
|
80
|
+
// Auto-repeat while held: swallow it and keep the release timer alive.
|
|
81
|
+
this.#armReleaseTimer();
|
|
82
|
+
return "swallow";
|
|
83
|
+
}
|
|
84
|
+
// Any non-space means the bar was released — stop recording, then let the key through.
|
|
85
|
+
this.#end();
|
|
86
|
+
return "pass";
|
|
87
|
+
}
|
|
88
|
+
if (!isSpace) {
|
|
89
|
+
this.#resetRun();
|
|
90
|
+
return "pass";
|
|
91
|
+
}
|
|
92
|
+
if (!(this.handler?.enabled() && this.#allowed())) return "pass";
|
|
93
|
+
const now = performance.now();
|
|
94
|
+
const gap = now - this.#lastSpaceAt;
|
|
95
|
+
const prevGap = this.#prevGap;
|
|
96
|
+
this.#lastSpaceAt = now;
|
|
97
|
+
this.#prevGap = gap;
|
|
98
|
+
if (prevGap === undefined || !gapsAreMechanical(gap, prevGap)) {
|
|
99
|
+
// First space, a deliberate tap, or jittery smashing: not a steady machine cadence yet, so
|
|
100
|
+
// type a real space and reset the mechanical run.
|
|
101
|
+
this.#mechanicalRun = 0;
|
|
102
|
+
this.#typed++;
|
|
103
|
+
return "type";
|
|
104
|
+
}
|
|
105
|
+
// Steady fast repeat: swallow it. Once the cadence has held for SPACE_HOLD_MECHANICAL_RUN
|
|
106
|
+
// deltas it's a held bar — track back the few pre-burst spaces already typed and start.
|
|
107
|
+
if (++this.#mechanicalRun >= SPACE_HOLD_MECHANICAL_RUN) {
|
|
108
|
+
this.#deleteTyped(this.#typed);
|
|
109
|
+
this.#resetRun();
|
|
110
|
+
this.#active = true;
|
|
111
|
+
this.#armReleaseTimer();
|
|
112
|
+
this.handler?.onStart();
|
|
113
|
+
}
|
|
114
|
+
return "swallow";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#resetRun(): void {
|
|
118
|
+
this.#typed = 0;
|
|
119
|
+
this.#mechanicalRun = 0;
|
|
120
|
+
this.#prevGap = undefined;
|
|
121
|
+
this.#lastSpaceAt = Number.NEGATIVE_INFINITY;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#armReleaseTimer(): void {
|
|
125
|
+
if (this.#releaseTimer) clearTimeout(this.#releaseTimer);
|
|
126
|
+
this.#releaseTimer = setTimeout(() => {
|
|
127
|
+
this.#releaseTimer = undefined;
|
|
128
|
+
this.#end();
|
|
129
|
+
}, SPACE_HOLD_RELEASE_MS);
|
|
130
|
+
this.#releaseTimer.unref?.();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#end(): void {
|
|
134
|
+
if (!this.#active) return;
|
|
135
|
+
this.#active = false;
|
|
136
|
+
this.#resetRun();
|
|
137
|
+
if (this.#releaseTimer) {
|
|
138
|
+
clearTimeout(this.#releaseTimer);
|
|
139
|
+
this.#releaseTimer = undefined;
|
|
140
|
+
}
|
|
141
|
+
this.handler?.onEnd();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -301,6 +301,7 @@ interface StatusLineExternalInputs {
|
|
|
301
301
|
isStreaming: boolean | undefined;
|
|
302
302
|
isAutoThinking: boolean | undefined;
|
|
303
303
|
isFastModeActive: boolean;
|
|
304
|
+
anthropicSlowModeLabel: string | undefined;
|
|
304
305
|
compactionSpeculation: unknown;
|
|
305
306
|
}
|
|
306
307
|
|
|
@@ -2369,6 +2370,10 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2369
2370
|
isAutoThinking: this.session.isAutoThinking,
|
|
2370
2371
|
isFastModeActive:
|
|
2371
2372
|
typeof this.session.isFastModeActive === "function" ? this.session.isFastModeActive() : false,
|
|
2373
|
+
anthropicSlowModeLabel:
|
|
2374
|
+
typeof this.session.getAnthropicSlowModeLabel === "function"
|
|
2375
|
+
? this.session.getAnthropicSlowModeLabel()
|
|
2376
|
+
: undefined,
|
|
2372
2377
|
compactionSpeculation: this.session.compactionSpeculation,
|
|
2373
2378
|
};
|
|
2374
2379
|
}
|
|
@@ -2423,6 +2428,7 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2423
2428
|
left.isStreaming === right.isStreaming &&
|
|
2424
2429
|
left.isAutoThinking === right.isAutoThinking &&
|
|
2425
2430
|
left.isFastModeActive === right.isFastModeActive &&
|
|
2431
|
+
left.anthropicSlowModeLabel === right.anthropicSlowModeLabel &&
|
|
2426
2432
|
left.compactionSpeculation === right.compactionSpeculation
|
|
2427
2433
|
);
|
|
2428
2434
|
}
|
package/src/status-line/host.ts
CHANGED
|
@@ -53,6 +53,8 @@ export interface StatusLineSession {
|
|
|
53
53
|
getContextUsage(): { tokens: number; contextWindow: number; percent: number | null } | undefined;
|
|
54
54
|
autoResolvedThinkingLevel(): string | undefined;
|
|
55
55
|
isFastModeActive(): boolean;
|
|
56
|
+
/** Anthropic usage-limit label (`limit reached · wrapping up · resets 14:30`, `low priority until 14:30 · 62% left`). */
|
|
57
|
+
getAnthropicSlowModeLabel?(): string | undefined;
|
|
56
58
|
getPrewalkState?(): unknown;
|
|
57
59
|
getAsyncJobSnapshot(): { running: readonly { type: string; agentId?: string }[] } | null | undefined;
|
|
58
60
|
getGoalModeState(): { goal?: { status: string; tokensUsed: number; tokenBudget?: number } } | undefined;
|
|
@@ -293,6 +293,10 @@ const modelSegment: StatusLineSegment = {
|
|
|
293
293
|
content += accentFg(ctx, "statusLineModel", tail);
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
// Anthropic usage-limit stage (wrap-up allowance or low priority): a
|
|
297
|
+
// warning-colored badge so past-the-limit service is never mistaken for normal.
|
|
298
|
+
const slowModeLabel = ctx.session.getAnthropicSlowModeLabel?.();
|
|
299
|
+
if (slowModeLabel) content += theme.fg("warning", `${theme.sep.dot}${slowModeLabel}`);
|
|
296
300
|
return { content, visible: true };
|
|
297
301
|
},
|
|
298
302
|
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript renderers for `cfg://` settings reads and writes.
|
|
3
|
+
*
|
|
4
|
+
* Reads show the YAML-ish settings listing the model received, highlighted as
|
|
5
|
+
* YAML. Writes show the `previous → value` change, its scope (session or
|
|
6
|
+
* saved), and whether the user approved it.
|
|
7
|
+
*/
|
|
8
|
+
import type { Component } from "../tui";
|
|
9
|
+
import { renderStatusLine } from "../render";
|
|
10
|
+
import {
|
|
11
|
+
cappedHeadLines,
|
|
12
|
+
formatBadge,
|
|
13
|
+
formatErrorDetail,
|
|
14
|
+
formatMoreItems,
|
|
15
|
+
PREVIEW_LIMITS,
|
|
16
|
+
} from "../render/render-utils";
|
|
17
|
+
import { highlightCode, type Theme } from "../theme/theme";
|
|
18
|
+
import { parseCfgUrl } from "./cfg-url";
|
|
19
|
+
import type { RenderResultOptions } from "./renderer";
|
|
20
|
+
import { card, type CardToolResult, firstText, safe } from "./result-card";
|
|
21
|
+
|
|
22
|
+
/** Summary of a `cfg://` read, attached to read tool details. */
|
|
23
|
+
export interface CfgReadDetails {
|
|
24
|
+
/** Dotted setting path or namespace that was read; empty for the whole tree. */
|
|
25
|
+
path: string;
|
|
26
|
+
/** Settings in the listing. */
|
|
27
|
+
count: number;
|
|
28
|
+
/** Listed settings whose effective value differs from the schema default. */
|
|
29
|
+
modified: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** How a `cfg://` write ended. */
|
|
33
|
+
export type CfgWriteOutcome = "applied" | "declined" | "unchanged";
|
|
34
|
+
|
|
35
|
+
/** A `cfg://` write, attached to write tool details. Values are display-formatted (credentials redacted). */
|
|
36
|
+
export interface CfgWriteDetails {
|
|
37
|
+
path: string;
|
|
38
|
+
/** Persisted to config.yml rather than scoped to the session. */
|
|
39
|
+
save: boolean;
|
|
40
|
+
previous: string;
|
|
41
|
+
value: string;
|
|
42
|
+
outcome: CfgWriteOutcome;
|
|
43
|
+
/** Effective value after saving when a higher-precedence layer still shadows the saved one. */
|
|
44
|
+
effective?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Render `read cfg://…`: pending header, or the highlighted listing with counts. */
|
|
48
|
+
export function renderCfgRead(
|
|
49
|
+
url: string,
|
|
50
|
+
result: CardToolResult | undefined,
|
|
51
|
+
details: CfgReadDetails | undefined,
|
|
52
|
+
options: RenderResultOptions,
|
|
53
|
+
theme: Theme,
|
|
54
|
+
): Component {
|
|
55
|
+
return card((_width, expanded) => {
|
|
56
|
+
const meta: string[] = [];
|
|
57
|
+
if (details) {
|
|
58
|
+
meta.push(details.count === 1 ? "1 setting" : `${details.count} settings`);
|
|
59
|
+
if (details.modified) meta.push(`${details.modified} modified`);
|
|
60
|
+
}
|
|
61
|
+
const header = renderStatusLine(
|
|
62
|
+
{
|
|
63
|
+
icon: result === undefined ? "pending" : result.isError ? "error" : "info",
|
|
64
|
+
title: "Config",
|
|
65
|
+
description: (details?.path ?? parseCfgUrl(url)?.segments.join(".")) || "all settings",
|
|
66
|
+
meta,
|
|
67
|
+
},
|
|
68
|
+
theme,
|
|
69
|
+
);
|
|
70
|
+
if (!result) return [header];
|
|
71
|
+
if (result.isError) return [header, formatErrorDetail(firstText(result) || "Settings read failed.", theme)];
|
|
72
|
+
const body = firstText(result).trimEnd().split("\n");
|
|
73
|
+
const shown = cappedHeadLines(body, expanded ? body.length : PREVIEW_LIMITS.COLLAPSED_ITEMS);
|
|
74
|
+
const lines = [header, ...highlightCode(safe(shown.lines.join("\n")), "yaml", theme).map(line => ` ${line}`)];
|
|
75
|
+
if (shown.hidden) lines.push(theme.fg("dim", ` ${formatMoreItems(shown.hidden, "line")}`));
|
|
76
|
+
return lines;
|
|
77
|
+
}, options);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Render `write cfg://…[/save]`: the proposed value while pending, then the approved/declined change. */
|
|
81
|
+
export function renderCfgWrite(
|
|
82
|
+
url: string,
|
|
83
|
+
content: string | undefined,
|
|
84
|
+
result: CardToolResult | undefined,
|
|
85
|
+
details: CfgWriteDetails | undefined,
|
|
86
|
+
options: RenderResultOptions,
|
|
87
|
+
theme: Theme,
|
|
88
|
+
): Component {
|
|
89
|
+
return card(() => {
|
|
90
|
+
const target = parseCfgUrl(url);
|
|
91
|
+
const save = details?.save ?? target?.save ?? false;
|
|
92
|
+
const icon =
|
|
93
|
+
result === undefined
|
|
94
|
+
? "pending"
|
|
95
|
+
: result.isError
|
|
96
|
+
? "error"
|
|
97
|
+
: details?.outcome === "declined"
|
|
98
|
+
? "warning"
|
|
99
|
+
: "success";
|
|
100
|
+
const badges = [formatBadge(save ? "persist" : "session", save ? "accent" : "muted", theme)];
|
|
101
|
+
if (details?.outcome === "declined") badges.push(formatBadge("declined", "warning", theme));
|
|
102
|
+
if (details?.outcome === "unchanged") badges.push(formatBadge("unchanged", "muted", theme));
|
|
103
|
+
const header = `${renderStatusLine({ icon, title: "Config", description: details?.path ?? target?.segments.join(".") }, theme)} ${badges.join(" ")}`;
|
|
104
|
+
if (result?.isError) return [header, formatErrorDetail(firstText(result) || "Settings write failed.", theme)];
|
|
105
|
+
const arrow = theme.fg("dim", "→");
|
|
106
|
+
const next = theme.fg("toolOutput", details?.value ?? content?.trim() ?? "…");
|
|
107
|
+
const lines = [
|
|
108
|
+
header,
|
|
109
|
+
details ? ` ${theme.fg("dim", details.previous)} ${arrow} ${next}` : ` ${arrow} ${next}`,
|
|
110
|
+
];
|
|
111
|
+
if (details?.effective !== undefined)
|
|
112
|
+
lines.push(
|
|
113
|
+
theme.fg("warning", ` still ${details.effective}: a higher-precedence layer overrides the saved value`),
|
|
114
|
+
);
|
|
115
|
+
return lines;
|
|
116
|
+
}, options);
|
|
117
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Canonical prefix for settings URLs. */
|
|
2
|
+
export const CFG_URL_PREFIX = "cfg://";
|
|
3
|
+
|
|
4
|
+
/** Trailing write segment that persists a `cfg://` change instead of scoping it to the session. */
|
|
5
|
+
export const CFG_SAVE_SEGMENT = "save";
|
|
6
|
+
|
|
7
|
+
/** Setting addressed by a `cfg://` URL. */
|
|
8
|
+
export interface CfgTarget {
|
|
9
|
+
/** Setting path segments; empty for the root. `/` and `.` both separate segments. */
|
|
10
|
+
segments: string[];
|
|
11
|
+
/** Whether the URL ends in the `/save` suffix, which is removed from {@link segments}. */
|
|
12
|
+
save: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse a `cfg://` URL (`cfg://advisor/enabled`, `cfg://advisor.enabled/save`).
|
|
17
|
+
* Returns `null` for other schemes; query and fragment are ignored.
|
|
18
|
+
*/
|
|
19
|
+
export function parseCfgUrl(input: string): CfgTarget | null {
|
|
20
|
+
const trimmed = input.trim();
|
|
21
|
+
if (!trimmed.toLowerCase().startsWith(CFG_URL_PREFIX)) return null;
|
|
22
|
+
const segments = trimmed
|
|
23
|
+
.slice(CFG_URL_PREFIX.length)
|
|
24
|
+
.replace(/[?#].*$/s, "")
|
|
25
|
+
.split(/[/.]/)
|
|
26
|
+
.filter(Boolean);
|
|
27
|
+
const save = segments.length > 1 && segments.at(-1) === CFG_SAVE_SEGMENT;
|
|
28
|
+
if (save) segments.pop();
|
|
29
|
+
return { segments, save };
|
|
30
|
+
}
|
package/src/tools/find.ts
CHANGED
|
@@ -28,6 +28,7 @@ import type { Theme, ThemeColor } from "../theme/theme";
|
|
|
28
28
|
import type { Component } from "../tui";
|
|
29
29
|
import type { OutputMeta } from "./output-meta";
|
|
30
30
|
import type { RenderResultOptions, ToolRenderer } from "./renderer";
|
|
31
|
+
import { splitUrlScheme } from "./url-scheme-host";
|
|
31
32
|
|
|
32
33
|
/** A verified line range with its yes-probability and a one-line preview. */
|
|
33
34
|
export interface FindRange {
|
|
@@ -39,7 +40,7 @@ export interface FindRange {
|
|
|
39
40
|
|
|
40
41
|
/** A file whose verified passages cleared the threshold; `ranges` are merged positive spans, strongest first. */
|
|
41
42
|
export interface FindHit {
|
|
42
|
-
/** Display path relative to {@link FindToolDetails.cwd}, or an
|
|
43
|
+
/** Display path relative to {@link FindToolDetails.cwd}, or an internal URL under URL scopes. */
|
|
43
44
|
rel: string;
|
|
44
45
|
/** Filename judgment, when the name batch answered. */
|
|
45
46
|
nameScore?: number;
|
|
@@ -85,7 +86,7 @@ export interface FindToolDetails {
|
|
|
85
86
|
elapsedMs: number;
|
|
86
87
|
/** Session cwd; hit paths are relative to it. */
|
|
87
88
|
cwd: string;
|
|
88
|
-
/** Display form of the searched directory when narrower than cwd. */
|
|
89
|
+
/** Display form of the searched directory or file when narrower than cwd. */
|
|
89
90
|
scopePath?: string;
|
|
90
91
|
meta?: OutputMeta;
|
|
91
92
|
}
|
|
@@ -124,11 +125,11 @@ function gauge(p: number, theme: Theme): string {
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
function renderHit(hit: FindHit, rangeLimit: number, cwd: string | undefined, theme: Theme): string[] {
|
|
127
|
-
// `
|
|
128
|
-
// itself instead of joining it onto a filesystem base.
|
|
129
|
-
const
|
|
128
|
+
// `scheme://` hits (e.g. virtual docs) are not files under `cwd`: link the
|
|
129
|
+
// URL itself instead of joining it onto a filesystem base.
|
|
130
|
+
const isUrlHit = splitUrlScheme(hit.rel) !== undefined;
|
|
130
131
|
const link = (text: string, line?: number): string => {
|
|
131
|
-
if (
|
|
132
|
+
if (isUrlHit) return uriHyperlink(line === undefined ? hit.rel : `${hit.rel}:${line}`, text);
|
|
132
133
|
const absPath = cwd === undefined ? undefined : path.join(cwd, hit.rel);
|
|
133
134
|
return absPath === undefined ? text : fileHyperlink(absPath, text, { line });
|
|
134
135
|
};
|
package/src/tools/output-meta.ts
CHANGED
|
@@ -72,6 +72,8 @@ export interface OutputMeta {
|
|
|
72
72
|
/** Capture failure of this output itself; aggregate reports keep source failures on their entries. */
|
|
73
73
|
artifactError?: OutputArtifactError;
|
|
74
74
|
source?: SourceMeta;
|
|
75
|
+
/** The output is a bounded page of session artifact storage its `source` re-reads with line selectors; spill-to-artifact skips it. */
|
|
76
|
+
pagedSource?: true;
|
|
75
77
|
diagnostics?: DiagnosticMeta;
|
|
76
78
|
limits?: LimitsMeta;
|
|
77
79
|
}
|
package/src/tools/proc-render.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Component } from "../tui";
|
|
2
|
-
import {
|
|
2
|
+
import { Ellipsis, renderStatusLine, renderTreeList, truncateToWidth } from "../render";
|
|
3
3
|
import {
|
|
4
4
|
cappedHeadLines,
|
|
5
5
|
formatBadge,
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
formatStatusIcon,
|
|
9
9
|
PREVIEW_LIMITS,
|
|
10
10
|
TRUNCATE_LENGTHS,
|
|
11
|
-
replaceTabs,
|
|
12
11
|
type ToolUIColor,
|
|
13
12
|
} from "../render/render-utils";
|
|
14
13
|
import type { Theme } from "../theme/theme";
|
|
@@ -17,6 +16,7 @@ import type { AgentActivitySnapshot, CoordinationDetails, JobSnapshot } from "./
|
|
|
17
16
|
import type { IrcDeliveryReceipt } from "./irc";
|
|
18
17
|
import type { DaemonSnapshot } from "./daemon";
|
|
19
18
|
import { styleTerminalRow } from "./terminal-output";
|
|
19
|
+
import { card, type CardToolResult as ToolResult, firstText, safe } from "./result-card";
|
|
20
20
|
|
|
21
21
|
export interface ProcReadDetails {
|
|
22
22
|
jobs?: JobSnapshot[];
|
|
@@ -35,30 +35,6 @@ export type ProcWriteDetails =
|
|
|
35
35
|
/** Process operation selected by a write URL, independent of its content. */
|
|
36
36
|
export type ProcWriteAction = "stdin" | "mode" | "kill";
|
|
37
37
|
|
|
38
|
-
interface ToolResult {
|
|
39
|
-
content: Array<{ type: string; text?: string }>;
|
|
40
|
-
isError?: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function firstText(result: ToolResult): string {
|
|
44
|
-
return result.content.find(item => item.type === "text")?.text ?? "";
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function safe(value: string): string {
|
|
48
|
-
return replaceTabs(value).replace(/\r/g, "");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function card(lines: (width: number, expanded: boolean) => string[], options: RenderResultOptions): Component {
|
|
52
|
-
return createCachedComponent(
|
|
53
|
-
() => Boolean(options.expanded),
|
|
54
|
-
(width, expanded) =>
|
|
55
|
-
lines(width, expanded)
|
|
56
|
-
.flatMap(line => line.split("\n"))
|
|
57
|
-
.map(line => truncateToWidth(safe(line), width, Ellipsis.Unicode)),
|
|
58
|
-
{ paddingX: 1 },
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
38
|
function preview(body: string, expanded: boolean, theme: Theme, tone: "dim" | "toolOutput" = "dim"): string[] {
|
|
63
39
|
if (!body.trim()) return [];
|
|
64
40
|
const limit = expanded ? PREVIEW_LIMITS.EXPANDED_LINES : PREVIEW_LIMITS.COLLAPSED_LINES;
|