@oh-my-pi/pi-tui 18.2.11 → 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.
Files changed (101) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/THIRD-PARTY-NOTICES.txt +3 -3
  3. package/dist/types/apps/ps-data.d.ts +1 -1
  4. package/dist/types/apps/ps-top.d.ts +1 -1
  5. package/dist/types/chat/chat-transcript-builder.d.ts +1 -1
  6. package/dist/types/chat/read-tool-group.d.ts +3 -3
  7. package/dist/types/chat/user-message.d.ts +4 -1
  8. package/dist/types/components/editor.d.ts +0 -2
  9. package/dist/types/components/input.d.ts +16 -0
  10. package/dist/types/overlays/annotation-overlay.d.ts +30 -0
  11. package/dist/types/overlays/annotation-types.d.ts +84 -0
  12. package/dist/types/overlays/btw-history-panel.d.ts +4 -1
  13. package/dist/types/overlays/copy-selector.d.ts +30 -2
  14. package/dist/types/overlays/copy-targets.d.ts +2 -0
  15. package/dist/types/overlays/model-hub.d.ts +2 -2
  16. package/dist/types/overlays/oauth-selector.d.ts +3 -6
  17. package/dist/types/overlays/settings-defs.d.ts +5 -0
  18. package/dist/types/overlays/usage-dashboard.d.ts +2 -0
  19. package/dist/types/prompt/composer.d.ts +12 -1
  20. package/dist/types/prompt/custom-editor.d.ts +5 -26
  21. package/dist/types/prompt/image-source.d.ts +9 -8
  22. package/dist/types/space-hold.d.ts +45 -0
  23. package/dist/types/status-line/context-usage.d.ts +3 -1
  24. package/dist/types/status-line/host.d.ts +2 -0
  25. package/dist/types/theme/mermaid-cache.d.ts +4 -6
  26. package/dist/types/tools/bash.d.ts +7 -0
  27. package/dist/types/tools/cfg-render.d.ts +37 -0
  28. package/dist/types/tools/cfg-url.d.ts +16 -0
  29. package/dist/types/tools/daemon.d.ts +46 -0
  30. package/dist/types/tools/find.d.ts +2 -2
  31. package/dist/types/tools/irc.d.ts +30 -0
  32. package/dist/types/tools/line-ranges.d.ts +1 -1
  33. package/dist/types/tools/output-meta.d.ts +2 -0
  34. package/dist/types/tools/proc-render.d.ts +27 -0
  35. package/dist/types/tools/read.d.ts +18 -4
  36. package/dist/types/tools/result-card.d.ts +20 -0
  37. package/dist/types/tools/url-scheme-host.d.ts +29 -0
  38. package/dist/types/tools/wait.d.ts +131 -0
  39. package/dist/types/tools/write.d.ts +6 -0
  40. package/dist/types/tools/xd-url.d.ts +8 -0
  41. package/dist/types/tools/xdev.d.ts +9 -1
  42. package/dist/types/windows-altgr.d.ts +54 -0
  43. package/package.json +13 -10
  44. package/src/apps/debug/protocol-probe.ts +1 -1
  45. package/src/apps/ps-data.ts +1 -1
  46. package/src/apps/ps-top.ts +1 -1
  47. package/src/chat/chat-transcript-builder.ts +9 -5
  48. package/src/chat/read-tool-group.ts +8 -9
  49. package/src/chat/tool-execution.ts +15 -13
  50. package/src/chat/transcript-render-helpers.ts +3 -3
  51. package/src/chat/user-message.ts +16 -6
  52. package/src/components/editor.ts +2 -4
  53. package/src/components/input.ts +103 -30
  54. package/src/components/markdown.ts +19 -7
  55. package/src/desktop-notify.ts +1 -1
  56. package/src/overlays/annotation-overlay.ts +1144 -0
  57. package/src/overlays/annotation-types.ts +78 -0
  58. package/src/overlays/btw-history-panel.ts +4 -0
  59. package/src/overlays/copy-selector.ts +45 -19
  60. package/src/overlays/copy-targets.ts +3 -3
  61. package/src/overlays/model-hub.ts +5 -4
  62. package/src/overlays/oauth-selector.ts +8 -12
  63. package/src/overlays/plan-review-overlay.ts +213 -40
  64. package/src/overlays/settings-defs.ts +6 -0
  65. package/src/overlays/settings-selector.ts +4 -3
  66. package/src/overlays/usage-dashboard.ts +22 -1
  67. package/src/prompt/composer.ts +83 -56
  68. package/src/prompt/custom-editor.ts +15 -137
  69. package/src/prompt/image-source.ts +9 -8
  70. package/src/prompt/tips.txt +1 -0
  71. package/src/setup/scenes/sign-in.ts +1 -1
  72. package/src/space-hold.ts +143 -0
  73. package/src/status-line/component.ts +6 -0
  74. package/src/status-line/context-usage.ts +15 -3
  75. package/src/status-line/host.ts +2 -0
  76. package/src/status-line/segments.ts +4 -0
  77. package/src/terminal-capabilities.ts +2 -2
  78. package/src/terminal.ts +8 -1
  79. package/src/theme/mermaid-cache.ts +83 -25
  80. package/src/tools/bash.ts +14 -1
  81. package/src/tools/cfg-render.ts +117 -0
  82. package/src/tools/cfg-url.ts +30 -0
  83. package/src/tools/daemon.ts +51 -0
  84. package/src/tools/find.ts +7 -6
  85. package/src/tools/index.ts +2 -2
  86. package/src/tools/irc.ts +33 -0
  87. package/src/tools/line-ranges.ts +13 -3
  88. package/src/tools/output-meta.ts +2 -0
  89. package/src/tools/proc-render.ts +270 -0
  90. package/src/tools/read.ts +118 -56
  91. package/src/tools/result-card.ts +36 -0
  92. package/src/tools/url-scheme-host.ts +42 -0
  93. package/src/tools/wait.ts +595 -0
  94. package/src/tools/write.ts +120 -1
  95. package/src/tools/xd-url.ts +9 -0
  96. package/src/tools/xdev.ts +9 -1
  97. package/src/windows-altgr.ts +227 -0
  98. package/dist/types/chat/read-target.d.ts +0 -4
  99. package/dist/types/tools/hub.d.ts +0 -342
  100. package/src/chat/read-target.ts +0 -7
  101. package/src/tools/hub.ts +0 -1492
@@ -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
- /** Fired when a sustained space-bar hold is recognized — the push-to-talk STT start. The
906
- * optimistically-typed spaces have already been deleted by the time this runs. */
907
- onSpaceHoldStart?: () => void;
908
- /** Fired when the held space bar is released (detected as an idle gap with no further repeated
909
- * spaces) — the push-to-talk STT stop. */
910
- onSpaceHoldEnd?: () => void;
911
- /** Gate for the space-hold gesture. Returns false to keep the space bar inserting spaces
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
- if (this.#handleSpaceHold(data, canonical)) return;
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.
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Original filesystem source carried on image attachments that entered from a
3
- * file on disk: path-pasted/drag-and-dropped images and generated video
4
- * contact-sheet previews. Symbol metadata stays out of serialized/model-bound
5
- * image data while traveling with the draft object, until AgentSession creates
6
- * the hidden companion message that tells the model the path (and link
7
- * materialization prefers it over a blob copy).
2
+ * File source carried on image attachments backed by a file on disk:
3
+ * path-pasted/drag-and-dropped images, clipboard images the coding agent commits
4
+ * to the session's `local://` root, and generated video contact-sheet previews.
5
+ * Symbol metadata stays out of serialized/model-bound image data while traveling
6
+ * with the draft object, until AgentSession creates the hidden companion message
7
+ * that tells the model the path (and link materialization prefers it over a blob copy).
8
8
  */
9
9
  import type { ImageContent } from "@oh-my-pi/pi-ai";
10
10
  import { isRecord } from "@oh-my-pi/pi-utils";
@@ -12,8 +12,9 @@ import { isRecord } from "@oh-my-pi/pi-utils";
12
12
  /** How an image attachment's backing file entered the session. */
13
13
  export type ImageAttachmentSourceKind = "image" | "video";
14
14
 
15
- /** Original local file backing an image attachment. */
15
+ /** Local file backing an image attachment. */
16
16
  export interface ImageAttachmentSource {
17
+ /** Absolute filesystem path, or a session-relative `local://` URL for images committed to the session. */
17
18
  readonly path: string;
18
19
  readonly kind: ImageAttachmentSourceKind;
19
20
  }
@@ -43,7 +44,7 @@ function isImageAttachmentSource(value: unknown): value is ImageAttachmentSource
43
44
  return isRecord(value) && typeof value.path === "string" && (value.kind === "image" || value.kind === "video");
44
45
  }
45
46
 
46
- /** Return the original local source of an image attachment, or undefined for payloads with no source file (clipboard bitmaps). */
47
+ /** Return the local file backing an image attachment, or undefined for payloads with no file on disk. */
47
48
  export function imageAttachmentSource(image: ImageContent): ImageAttachmentSource | undefined {
48
49
  if (!(kImageAttachmentSource in image)) return undefined;
49
50
  const source = image[kImageAttachmentSource];
@@ -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
@@ -230,7 +230,7 @@ export class SignInTab implements SetupTab {
230
230
  this.#host.restoreFocus();
231
231
  this.#host.requestRender();
232
232
  try {
233
- await this.#authStorage.login(providerId as OAuthProvider, {
233
+ await this.#authStorage.oauth.login(providerId as OAuthProvider, {
234
234
  signal: this.#loginAbort.signal,
235
235
  onBrowserSession: (request, signal) => this.#host.ctx.captureBrowserSession(request, signal),
236
236
  onAuth: info => {
@@ -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
  }
@@ -2,6 +2,7 @@ import type { AgentMessage, Tokenizer } from "@oh-my-pi/pi-agent-core";
2
2
  import type { CompactionSettings } from "@oh-my-pi/pi-agent-core/compaction";
3
3
  import { effectiveReserveTokens, resolveThresholdTokens } from "@oh-my-pi/pi-agent-core/compaction";
4
4
  import type { Tool as AiTool, Model } from "@oh-my-pi/pi-ai";
5
+ import { renderToolExamples } from "@oh-my-pi/pi-ai/dialect";
5
6
  import { toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
6
7
  import { formatNumber } from "@oh-my-pi/pi-utils";
7
8
  import type { Theme } from "../theme";
@@ -12,7 +13,7 @@ interface ContextSkill {
12
13
  readonly hide?: boolean;
13
14
  }
14
15
 
15
- type ContextTool = Pick<AiTool, "name" | "description" | "parameters">;
16
+ type ContextTool = Pick<AiTool, "name" | "description" | "parameters" | "examples">;
16
17
 
17
18
  /** Savings computed by the host's inline-image planner, not by the renderer. */
18
19
  export interface ContextSavingsEstimate {
@@ -140,6 +141,8 @@ export interface NonMessageTokenSource {
140
141
  };
141
142
  };
142
143
  readonly skills?: readonly ContextSkill[];
144
+ /** Provider-facing, session-frozen descriptions when available. */
145
+ readonly renderedSkills?: readonly ContextSkill[];
143
146
  }
144
147
 
145
148
  /** Shared empty system-prompt part list, avoiding an allocation per render. */
@@ -244,9 +247,13 @@ export function estimateToolSchemaTokens(tools: ToolSchemaSource, tokenizer: Tok
244
247
  name,
245
248
  description,
246
249
  parameters: parameters as AiTool["parameters"],
250
+ examples: tool.examples,
247
251
  };
248
252
  const wireJson = JSON.stringify(toolWireSchema(wireTool) ?? {});
249
253
  if (typeof wireJson === "string") fragments.push(wireJson);
254
+ // The agent loop appends rendered examples to the wire description.
255
+ const examplesBlock = renderToolExamples(wireTool);
256
+ if (examplesBlock) fragments.push(examplesBlock);
250
257
  } catch {
251
258
  // Schema may contain functions or cycles; ignore.
252
259
  }
@@ -313,7 +320,7 @@ function nonMessageTokenCacheEntry(
313
320
  const systemPromptRef = session.systemPrompt ?? EMPTY_STRING_PARTS;
314
321
  const toolsRef = session.agent?.state?.tools ?? EMPTY_TOOLS;
315
322
  const toolsRevision = getToolSchemaMetadataRevision(toolsRef);
316
- const skillsRef = session.skills ?? EMPTY_SKILLS;
323
+ const skillsRef = session.renderedSkills ?? session.skills ?? EMPTY_SKILLS;
317
324
  let entry = cachedSession[NON_MESSAGE_TOKEN_CACHE];
318
325
  if (
319
326
  entry &&
@@ -378,7 +385,12 @@ export function computeNonMessageBreakdown(
378
385
  if (entry.breakdown && entry.skillful === skillful) return entry.breakdown;
379
386
  const tools = session.agent?.state?.tools ?? EMPTY_TOOLS;
380
387
  const skillsTokens =
381
- skillful === false ? 0 : estimateSkillsTokens(renderedSkills(session.skills ?? EMPTY_SKILLS, tools), tokenizer);
388
+ skillful === false
389
+ ? 0
390
+ : estimateSkillsTokens(
391
+ renderedSkills(session.renderedSkills ?? session.skills ?? EMPTY_SKILLS, tools),
392
+ tokenizer,
393
+ );
382
394
  const toolsTokens = estimateToolSchemaTokens(tools, tokenizer, sourceRevision);
383
395
  const systemPromptParts = session.systemPrompt ?? EMPTY_STRING_PARTS;
384
396
  const systemContextTokens = tokenizer.countTokens(Array.from(systemPromptParts.slice(1), part => part ?? ""));
@@ -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
  };
@@ -42,7 +42,7 @@ export type TerminalId =
42
42
  | "base"
43
43
  | "trueColor";
44
44
 
45
- const CMUX_NOTIFICATION_TITLE = "Oh My Pi";
45
+ const CMUX_NOTIFICATION_TITLE = "omp";
46
46
  const CMUX_SURFACE_ID_PATTERN = /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu;
47
47
 
48
48
  /** Title and body for an out-of-band multiplexer notification (cmux, Herdr). */
@@ -1433,7 +1433,7 @@ function notificationToLine(n: TerminalNotification): string {
1433
1433
  // C0/C1 control characters that are unsafe inside an OSC payload (must base64).
1434
1434
  const OSC99_UNSAFE = /[\x00-\x1f\x7f\x80-\x9f]/u;
1435
1435
  const OSC99_MAX_PAYLOAD_BYTES = 2048;
1436
- const OSC99_APP_NAME = "Oh My Pi";
1436
+ const OSC99_APP_NAME = "omp";
1437
1437
  let nextOsc99NotificationId = 1;
1438
1438
 
1439
1439
  function base64Utf8(value: string): string {
package/src/terminal.ts CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  } from "./terminal-capabilities";
26
26
  import { isInsideTmux, wrapTmuxPassthrough } from "./tmux";
27
27
  import { setHangulCompatibilityJamoWidth } from "./utils";
28
+ import { translateWindowsAltGrSequence } from "./windows-altgr";
28
29
 
29
30
  const TERMINAL_PROGRESS_KEEPALIVE_MS = 1000;
30
31
  const TERMINAL_PROGRESS_ACTIVE_SEQUENCE = "\x1b]9;4;3\x07";
@@ -1463,7 +1464,13 @@ export class ProcessTerminal implements Terminal {
1463
1464
  return;
1464
1465
  }
1465
1466
  if (this.#inputHandler) {
1466
- this.#inputHandler(sequence);
1467
+ // Windows console hosts drop AltGr text under kitty (AltGr+F → `CSI 102;3u`);
1468
+ // recover it from the active layout before any keybinding sees an Alt chord.
1469
+ const altGrText =
1470
+ this.#kittyProtocolActive && this.#conpty && process.platform === "win32"
1471
+ ? translateWindowsAltGrSequence(sequence)
1472
+ : undefined;
1473
+ this.#inputHandler(altGrText ?? sequence);
1467
1474
  }
1468
1475
  });
1469
1476