@oh-my-pi/pi-tui 18.3.0 → 18.3.1
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/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/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
|
@@ -5,6 +5,7 @@ import { BRACKETED_PASTE_END, BRACKETED_PASTE_START } from "../stdin-buffer";
|
|
|
5
5
|
import { Editor, type EditorTextDecorationContext, type EditorTheme } from "../components/editor";
|
|
6
6
|
import { addKeyAliases, canonicalKeyId, getKeybindings } from "../keybindings";
|
|
7
7
|
import { type KeyId, parseKey, parseKittySequence } from "../keys";
|
|
8
|
+
import { SpaceHoldGesture } from "../space-hold";
|
|
8
9
|
import { TUI } from "../tui";
|
|
9
10
|
import type { AppKeybinding } from "../app-keybindings";
|
|
10
11
|
import { allowsModelMentions, allowsSkillTokens, SKILL_TOKEN_RE } from "./skill-tokens";
|
|
@@ -117,34 +118,6 @@ const ABSOLUTE_PATH_PREFIX_REGEX = new RegExp(`^${ABSOLUTE_PATH_PREFIX_SOURCE}`)
|
|
|
117
118
|
*/
|
|
118
119
|
const INTERIOR_PATH_ANCHOR_REGEX = new RegExp(String.raw`(?<!\\)\s(?:${ABSOLUTE_PATH_PREFIX_SOURCE}|\.\.?[\\/])`);
|
|
119
120
|
|
|
120
|
-
/** Max gap (ms) between two spaces for the later one to count as OS key auto-repeat rather than a
|
|
121
|
-
* deliberate press. OS auto-repeat is fast; a deliberate tap (even a fast one) is slower. */
|
|
122
|
-
export const SPACE_REPEAT_MAX_GAP_MS = 120;
|
|
123
|
-
/** Two consecutive inter-space gaps are "mechanical" (machine-driven auto-repeat) when both are
|
|
124
|
-
* within {@link SPACE_REPEAT_MAX_GAP_MS} and differ by no more than this — an absolute jitter floor
|
|
125
|
-
* or, for slower repeat rates, {@link SPACE_REPEAT_JITTER_RATIO} of the smaller gap. OS key-repeat
|
|
126
|
-
* is metronomic; a human smashing the bar is fast but irregular, so its deltas never stay this
|
|
127
|
-
* steady. */
|
|
128
|
-
export const SPACE_REPEAT_JITTER_MS = 18;
|
|
129
|
-
export const SPACE_REPEAT_JITTER_RATIO = 0.35;
|
|
130
|
-
/** Consecutive mechanical (fast + steady) deltas that confirm the space bar is held and start
|
|
131
|
-
* recording. Needs a sustained metronomic cadence, so jittery smashing and deliberate taps never
|
|
132
|
-
* reach it. */
|
|
133
|
-
export const SPACE_HOLD_MECHANICAL_RUN = 2;
|
|
134
|
-
/** Idle gap (ms) after the last repeated space that counts as the space bar being released, ending
|
|
135
|
-
* the push-to-talk recording. Must comfortably exceed the OS key-repeat interval. */
|
|
136
|
-
export const SPACE_HOLD_RELEASE_MS = 250;
|
|
137
|
-
|
|
138
|
-
/** Whether two consecutive inter-space gaps look machine-driven: both within the auto-repeat band
|
|
139
|
-
* and steady enough (small absolute or proportional difference). OS key-repeat is metronomic, so
|
|
140
|
-
* its successive deltas match closely; human smashing is fast but irregular and deliberate taps are
|
|
141
|
-
* too slow, so neither passes. */
|
|
142
|
-
function gapsAreMechanical(gap: number, prevGap: number): boolean {
|
|
143
|
-
if (gap > SPACE_REPEAT_MAX_GAP_MS || prevGap > SPACE_REPEAT_MAX_GAP_MS) return false;
|
|
144
|
-
const tolerance = Math.max(SPACE_REPEAT_JITTER_MS, Math.min(gap, prevGap) * SPACE_REPEAT_JITTER_RATIO);
|
|
145
|
-
return Math.abs(gap - prevGap) <= tolerance;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
121
|
function isPastedPathSeparator(char: string | undefined): boolean {
|
|
149
122
|
return char === undefined || char === " " || char === "\t" || char === "\r" || char === "\n";
|
|
150
123
|
}
|
|
@@ -902,15 +875,13 @@ export class CustomEditor extends Editor {
|
|
|
902
875
|
/** Called when left-arrow is pressed while the editor is empty (cursor necessarily at start). */
|
|
903
876
|
onLeftAtStart?: () => void;
|
|
904
877
|
|
|
905
|
-
/**
|
|
906
|
-
*
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
* normally; wired to `stt.enabled` so disabling STT restores plain space behavior. */
|
|
913
|
-
sttHoldEnabled?: () => boolean;
|
|
878
|
+
/** Space-bar push-to-talk; set its `handler` to enable it. It is a text-composition gesture, so it
|
|
879
|
+
* stays out of Vim's Normal/Visual modes (where the space bar is the `l` motion) and away from an
|
|
880
|
+
* open autocomplete menu. */
|
|
881
|
+
readonly spaceHold = new SpaceHoldGesture(
|
|
882
|
+
count => this.deleteBeforeCursor(count),
|
|
883
|
+
() => this.vimMode === "insert" && !this.isShowingAutocomplete(),
|
|
884
|
+
);
|
|
914
885
|
|
|
915
886
|
/** Custom key handlers from extensions and non-built-in app actions. */
|
|
916
887
|
#customKeyHandlers = new Map<KeyId, () => void>();
|
|
@@ -928,18 +899,6 @@ export class CustomEditor extends Editor {
|
|
|
928
899
|
/** Input chunks deferred behind an in-flight paste, drained in FIFO order once the paste
|
|
929
900
|
* count returns to zero. */
|
|
930
901
|
#pendingInput: string[] = [];
|
|
931
|
-
/** Spaces actually inserted in the current run; tracked back out when a hold is recognized. */
|
|
932
|
-
#spaceRunInserted = 0;
|
|
933
|
-
/** Consecutive "mechanical" deltas (fast + steady); a sustained run of these confirms a held bar. */
|
|
934
|
-
#mechanicalRun = 0;
|
|
935
|
-
/** Inter-space gap (ms) of the previous space pair, compared against the next to judge steadiness. */
|
|
936
|
-
#prevSpaceGap: number | undefined;
|
|
937
|
-
/** Monotonic timestamp (ms) of the last space, to measure the gap to the next one. */
|
|
938
|
-
#lastSpaceAt = Number.NEGATIVE_INFINITY;
|
|
939
|
-
/** True while a recognized space-hold push-to-talk recording is in progress. */
|
|
940
|
-
#spaceHoldActive = false;
|
|
941
|
-
/** Idle timer that fires `onSpaceHoldEnd` once repeated spaces stop arriving. */
|
|
942
|
-
#spaceHoldTimer: NodeJS.Timeout | undefined;
|
|
943
902
|
#actionKeys = new Map<ConfigurableEditorAction, KeyId[]>(
|
|
944
903
|
Object.entries(DEFAULT_ACTION_KEYS).map(([action, keys]) => [action as ConfigurableEditorAction, [...keys]]),
|
|
945
904
|
);
|
|
@@ -1004,93 +963,6 @@ export class CustomEditor extends Editor {
|
|
|
1004
963
|
this.#rebuildCustomMatchKeys();
|
|
1005
964
|
}
|
|
1006
965
|
|
|
1007
|
-
#spaceHoldGestureEnabled(): boolean {
|
|
1008
|
-
// Push-to-talk is a text-composition gesture, so it stays out of Vim's Normal/Visual modes
|
|
1009
|
-
// where the space bar is the `l` motion.
|
|
1010
|
-
if (this.vimMode !== "insert") return false;
|
|
1011
|
-
return this.onSpaceHoldStart !== undefined && (this.sttHoldEnabled?.() ?? false) && !this.isShowingAutocomplete();
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
/** Drive the space-hold push-to-talk state machine. Returns true when the gesture consumed the
|
|
1015
|
-
* input so it must not reach normal editing. A held space bar emits OS auto-repeat: a *steady*
|
|
1016
|
-
* stream of spaces at a fixed fast interval. We watch the inter-space deltas and only recognize a
|
|
1017
|
-
* hold once {@link SPACE_HOLD_MECHANICAL_RUN} consecutive deltas are "mechanical" — both
|
|
1018
|
-
* auto-repeat-fast and near-identical (see {@link gapsAreMechanical}). Smashing the bar is fast
|
|
1019
|
-
* but jittery and deliberate taps are too slow, so neither escalates and both keep typing real
|
|
1020
|
-
* spaces; the few spaces typed before a real hold is recognized are tracked back out. */
|
|
1021
|
-
#handleSpaceHold(data: string, canonical: string | undefined): boolean {
|
|
1022
|
-
const isSpace = canonical === "space";
|
|
1023
|
-
if (this.#spaceHoldActive) {
|
|
1024
|
-
if (isSpace) {
|
|
1025
|
-
// Auto-repeat while held: swallow it and keep the release timer alive.
|
|
1026
|
-
this.#armSpaceHoldReleaseTimer();
|
|
1027
|
-
return true;
|
|
1028
|
-
}
|
|
1029
|
-
// Any non-space means the bar was released — stop recording, then let the key through.
|
|
1030
|
-
this.#endSpaceHold();
|
|
1031
|
-
return false;
|
|
1032
|
-
}
|
|
1033
|
-
if (!isSpace) {
|
|
1034
|
-
this.#resetSpaceRun();
|
|
1035
|
-
return false;
|
|
1036
|
-
}
|
|
1037
|
-
if (!this.#spaceHoldGestureEnabled()) return false;
|
|
1038
|
-
const now = performance.now();
|
|
1039
|
-
const gap = now - this.#lastSpaceAt;
|
|
1040
|
-
const prevGap = this.#prevSpaceGap;
|
|
1041
|
-
this.#lastSpaceAt = now;
|
|
1042
|
-
this.#prevSpaceGap = gap;
|
|
1043
|
-
if (prevGap === undefined || !gapsAreMechanical(gap, prevGap)) {
|
|
1044
|
-
// First space, a deliberate tap, or jittery smashing: not a steady machine cadence yet, so
|
|
1045
|
-
// type a real space and reset the mechanical run.
|
|
1046
|
-
this.#mechanicalRun = 0;
|
|
1047
|
-
this.#forwardInput(data);
|
|
1048
|
-
this.#spaceRunInserted++;
|
|
1049
|
-
return true;
|
|
1050
|
-
}
|
|
1051
|
-
// Steady fast repeat: swallow it. Once the cadence has held for SPACE_HOLD_MECHANICAL_RUN
|
|
1052
|
-
// deltas it's a held bar — track back the few pre-burst spaces already typed and start.
|
|
1053
|
-
if (++this.#mechanicalRun >= SPACE_HOLD_MECHANICAL_RUN) {
|
|
1054
|
-
this.deleteBeforeCursor(this.#spaceRunInserted);
|
|
1055
|
-
this.#resetSpaceRun();
|
|
1056
|
-
this.#beginSpaceHold();
|
|
1057
|
-
}
|
|
1058
|
-
return true;
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
#resetSpaceRun(): void {
|
|
1062
|
-
this.#spaceRunInserted = 0;
|
|
1063
|
-
this.#mechanicalRun = 0;
|
|
1064
|
-
this.#prevSpaceGap = undefined;
|
|
1065
|
-
this.#lastSpaceAt = Number.NEGATIVE_INFINITY;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
#beginSpaceHold(): void {
|
|
1069
|
-
this.#spaceHoldActive = true;
|
|
1070
|
-
this.#armSpaceHoldReleaseTimer();
|
|
1071
|
-
this.onSpaceHoldStart?.();
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
#armSpaceHoldReleaseTimer(): void {
|
|
1075
|
-
if (this.#spaceHoldTimer) clearTimeout(this.#spaceHoldTimer);
|
|
1076
|
-
this.#spaceHoldTimer = setTimeout(() => {
|
|
1077
|
-
this.#spaceHoldTimer = undefined;
|
|
1078
|
-
this.#endSpaceHold();
|
|
1079
|
-
}, SPACE_HOLD_RELEASE_MS);
|
|
1080
|
-
this.#spaceHoldTimer.unref?.();
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
#endSpaceHold(): void {
|
|
1084
|
-
if (!this.#spaceHoldActive) return;
|
|
1085
|
-
this.#spaceHoldActive = false;
|
|
1086
|
-
this.#resetSpaceRun();
|
|
1087
|
-
if (this.#spaceHoldTimer) {
|
|
1088
|
-
clearTimeout(this.#spaceHoldTimer);
|
|
1089
|
-
this.#spaceHoldTimer = undefined;
|
|
1090
|
-
}
|
|
1091
|
-
this.onSpaceHoldEnd?.();
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
966
|
/** Decrement {@link #pasteInFlight} once an async paste settles and, when the count returns
|
|
1095
967
|
* to zero, drain {@link #pendingInput} through `handleInput` so requeueing still works if a
|
|
1096
968
|
* drained chunk triggers another async paste. Bound member so it can be passed straight to
|
|
@@ -1188,7 +1060,13 @@ export class CustomEditor extends Editor {
|
|
|
1188
1060
|
}
|
|
1189
1061
|
|
|
1190
1062
|
// Space-hold push-to-talk: a sustained space bar starts/stops STT instead of typing spaces.
|
|
1191
|
-
|
|
1063
|
+
switch (this.spaceHold.process(canonical === "space")) {
|
|
1064
|
+
case "type":
|
|
1065
|
+
this.#forwardInput(data);
|
|
1066
|
+
return;
|
|
1067
|
+
case "swallow":
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1192
1070
|
|
|
1193
1071
|
// One union probe decides whether any per-action interception below can
|
|
1194
1072
|
// match — plain typing then skips the ~20 per-action set lookups per key.
|
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 subscription slow-mode label (`low priority until 14:30 · 62% left`) while active. */
|
|
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 slow mode: a warning-colored badge so a throttled lane is never
|
|
297
|
+
// mistaken for normal service.
|
|
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;
|