@oh-my-pi/pi-coding-agent 15.11.6 → 15.11.8
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 +57 -1
- package/dist/cli.js +431 -381
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +78 -0
- package/dist/types/collab/crypto.d.ts +12 -0
- package/dist/types/collab/guest.d.ts +21 -0
- package/dist/types/collab/host.d.ts +13 -0
- package/dist/types/collab/protocol.d.ts +100 -0
- package/dist/types/collab/relay-client.d.ts +22 -0
- package/dist/types/commands/bench.d.ts +29 -0
- package/dist/types/commands/join.d.ts +12 -0
- package/dist/types/config/model-resolver.d.ts +3 -2
- package/dist/types/config/settings-schema.d.ts +93 -1
- package/dist/types/edit/renderer.d.ts +1 -0
- package/dist/types/extensibility/slash-commands.d.ts +1 -11
- package/dist/types/modes/components/agent-hub.d.ts +13 -0
- package/dist/types/modes/components/collab-prompt-message.d.ts +10 -0
- package/dist/types/modes/components/hook-selector.d.ts +4 -6
- package/dist/types/modes/components/oauth-selector.d.ts +10 -1
- package/dist/types/modes/components/segment-track.d.ts +11 -6
- package/dist/types/modes/components/settings-selector.d.ts +8 -1
- package/dist/types/modes/components/snapcompact-shape-preview.d.ts +31 -0
- package/dist/types/modes/components/status-line/component.d.ts +4 -1
- package/dist/types/modes/components/status-line/types.d.ts +9 -0
- package/dist/types/modes/components/tool-execution.d.ts +13 -9
- package/dist/types/modes/interactive-mode.d.ts +7 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +3 -0
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +10 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +3 -0
- package/dist/types/modes/types.d.ts +8 -0
- package/dist/types/session/agent-session.d.ts +11 -0
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/snapcompact-inline.d.ts +8 -3
- package/dist/types/slash-commands/builtin-registry.d.ts +9 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/eval-render.d.ts +1 -0
- package/dist/types/tools/renderers.d.ts +13 -0
- package/dist/types/tools/ssh.d.ts +1 -0
- package/package.json +14 -12
- package/scripts/bench-guard.ts +71 -0
- package/src/cli/args.ts +2 -0
- package/src/cli/bench-cli.ts +437 -0
- package/src/cli-commands.ts +2 -0
- package/src/collab/crypto.ts +57 -0
- package/src/collab/guest.ts +421 -0
- package/src/collab/host.ts +494 -0
- package/src/collab/protocol.ts +191 -0
- package/src/collab/relay-client.ts +216 -0
- package/src/commands/bench.ts +42 -0
- package/src/commands/join.ts +39 -0
- package/src/config/model-registry.ts +74 -19
- package/src/config/model-resolver.ts +36 -5
- package/src/config/settings-schema.ts +119 -1
- package/src/edit/renderer.ts +5 -0
- package/src/extensibility/slash-commands.ts +1 -97
- package/src/hindsight/client.ts +26 -1
- package/src/hindsight/state.ts +6 -2
- package/src/internal-urls/docs-index.generated.ts +4 -3
- package/src/main.ts +11 -2
- package/src/mcp/transports/stdio.ts +81 -7
- package/src/modes/components/agent-hub.ts +119 -22
- package/src/modes/components/assistant-message.ts +126 -6
- package/src/modes/components/collab-prompt-message.ts +30 -0
- package/src/modes/components/hook-selector.ts +4 -5
- package/src/modes/components/oauth-selector.ts +67 -7
- package/src/modes/components/segment-track.ts +44 -7
- package/src/modes/components/settings-selector.ts +27 -0
- package/src/modes/components/snapcompact-shape-preview-doc.md +11 -0
- package/src/modes/components/snapcompact-shape-preview.ts +192 -0
- package/src/modes/components/status-line/component.ts +21 -1
- package/src/modes/components/status-line/presets.ts +1 -1
- package/src/modes/components/status-line/segments.ts +13 -0
- package/src/modes/components/status-line/types.ts +10 -0
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +18 -10
- package/src/modes/controllers/input-controller.ts +80 -12
- package/src/modes/controllers/selector-controller.ts +6 -2
- package/src/modes/controllers/streaming-reveal.ts +7 -0
- package/src/modes/interactive-mode.ts +36 -4
- package/src/modes/setup-wizard/index.ts +1 -0
- package/src/modes/setup-wizard/scenes/glyph.ts +24 -6
- package/src/modes/setup-wizard/scenes/providers.ts +36 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +10 -1
- package/src/modes/setup-wizard/scenes/theme.ts +28 -1
- package/src/modes/setup-wizard/scenes/types.ts +10 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +22 -6
- package/src/modes/setup-wizard/wizard-overlay.ts +38 -1
- package/src/modes/types.ts +8 -0
- package/src/modes/utils/context-usage.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +7 -0
- package/src/prompts/bench.md +7 -0
- package/src/sdk.ts +240 -36
- package/src/session/agent-session.ts +22 -0
- package/src/session/session-manager.ts +44 -0
- package/src/session/snapcompact-inline.ts +20 -22
- package/src/slash-commands/builtin-registry.ts +210 -0
- package/src/tools/bash.ts +3 -0
- package/src/tools/eval-render.ts +4 -0
- package/src/tools/read.ts +38 -5
- package/src/tools/renderers.ts +13 -0
- package/src/tools/ssh.ts +3 -0
- package/src/tools/write.ts +13 -42
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import type { AutocompleteProvider, SlashCommand } from "@oh-my-pi/pi-tui";
|
|
4
|
-
import { $env, logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
5
|
-
import { getRoleInfo } from "../../config/model-roles";
|
|
4
|
+
import { $env, isEnoent, logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
6
5
|
import { isSettingsInitialized, settings } from "../../config/settings";
|
|
6
|
+
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
7
7
|
import { renderSegmentTrack } from "../../modes/components/segment-track";
|
|
8
8
|
import { TinyTitleDownloadProgressComponent } from "../../modes/components/tiny-title-download-progress";
|
|
9
9
|
import { expandEmoticons } from "../../modes/emoji-autocomplete";
|
|
@@ -17,6 +17,7 @@ import { isTinyTitleLocalModelKey } from "../../tiny/models";
|
|
|
17
17
|
import { isLowSignalTitleInput } from "../../tiny/text";
|
|
18
18
|
import { tinyTitleClient } from "../../tiny/title-client";
|
|
19
19
|
import type { TinyTitleProgressEvent } from "../../tiny/title-protocol";
|
|
20
|
+
import { shortenPath, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
|
|
20
21
|
import { copyToClipboard, readImageFromClipboard, readTextFromClipboard } from "../../utils/clipboard";
|
|
21
22
|
import { EnhancedPasteController } from "../../utils/enhanced-paste";
|
|
22
23
|
import { getEditorCommand, openInEditor } from "../../utils/external-editor";
|
|
@@ -124,6 +125,16 @@ export class InputController {
|
|
|
124
125
|
if (this.ctx.hasActiveOmfg() && this.ctx.handleOmfgEscape()) {
|
|
125
126
|
return;
|
|
126
127
|
}
|
|
128
|
+
if (this.ctx.collabGuest) {
|
|
129
|
+
// Guest Esc: ask the host to interrupt its agent; the local replica
|
|
130
|
+
// session is never streaming, so the native abort path below would
|
|
131
|
+
// no-op.
|
|
132
|
+
if (this.ctx.collabGuest.state?.isStreaming || this.ctx.loadingAnimation) {
|
|
133
|
+
this.ctx.notifyInterrupting();
|
|
134
|
+
this.ctx.collabGuest.sendAbort();
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
127
138
|
if (this.ctx.loadingAnimation) {
|
|
128
139
|
if (this.ctx.cancelPendingSubmission()) {
|
|
129
140
|
return;
|
|
@@ -391,6 +402,32 @@ export class InputController {
|
|
|
391
402
|
text = slashResult;
|
|
392
403
|
}
|
|
393
404
|
|
|
405
|
+
// Collab guest: prompts execute on the host; local slash/skill/bash/
|
|
406
|
+
// python execution is host-only (builtins are gated inside
|
|
407
|
+
// executeBuiltinSlashCommand, which already consumed allowed ones).
|
|
408
|
+
if (this.ctx.collabGuest) {
|
|
409
|
+
if (text.startsWith("/")) {
|
|
410
|
+
this.ctx.showStatus(`${text.split(/\s+/, 1)[0]} is host-only during a collab session`);
|
|
411
|
+
this.ctx.editor.setText("");
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (text.startsWith("!") || text.startsWith("$")) {
|
|
415
|
+
this.ctx.showStatus("Local execution is host-only during a collab session");
|
|
416
|
+
this.ctx.editor.setText("");
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
this.ctx.editor.addToHistory(text);
|
|
420
|
+
this.ctx.editor.setText("");
|
|
421
|
+
this.ctx.editor.imageLinks = undefined;
|
|
422
|
+
const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
|
|
423
|
+
this.ctx.pendingImages = [];
|
|
424
|
+
this.ctx.pendingImageLinks = [];
|
|
425
|
+
// No local render: the prompt comes back from the host as a
|
|
426
|
+
// collab-prompt event/entry and renders with the author badge.
|
|
427
|
+
this.ctx.collabGuest.sendPrompt(text, images);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
394
431
|
// Handle skill commands (/skill:name [args]). Enter ⇒ steer (matches the
|
|
395
432
|
// free-text Enter semantics applied a few lines below at the streaming
|
|
396
433
|
// branch). Ctrl+Enter routes through `handleFollowUp` and dispatches the
|
|
@@ -873,11 +910,41 @@ export class InputController {
|
|
|
873
910
|
`Unsupported pasted image format: ${image.mimeType}`,
|
|
874
911
|
);
|
|
875
912
|
} catch (error) {
|
|
913
|
+
if (error instanceof ImageInputTooLargeError) {
|
|
914
|
+
this.ctx.editor.pasteText(path);
|
|
915
|
+
this.ctx.ui.requestRender();
|
|
916
|
+
this.ctx.showStatus(error.message);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (isEnoent(error)) {
|
|
920
|
+
// #2375: the bracketed paste forwarded by a local terminal carries a
|
|
921
|
+
// path on the *local* filesystem. When omp itself runs over SSH, that
|
|
922
|
+
// path is unreachable here; pasting it as text would look like the
|
|
923
|
+
// image was attached when in fact nothing was sent. Refuse the silent
|
|
924
|
+
// degrade and tell the user how to send the bytes for real. The
|
|
925
|
+
// pasted path is untrusted terminal input — strip control/ANSI/
|
|
926
|
+
// newlines, collapse home to `~`, and bound the displayed length
|
|
927
|
+
// before splicing it into the status string.
|
|
928
|
+
const displayPath = truncateToWidth(
|
|
929
|
+
shortenPath(
|
|
930
|
+
sanitizeText(path)
|
|
931
|
+
.replace(/[\r\n\t]+/g, " ")
|
|
932
|
+
.trim(),
|
|
933
|
+
),
|
|
934
|
+
TRUNCATE_LENGTHS.CONTENT,
|
|
935
|
+
);
|
|
936
|
+
const env = process.env;
|
|
937
|
+
const overSsh = Boolean(env.SSH_CONNECTION || env.SSH_TTY || env.SSH_CLIENT);
|
|
938
|
+
this.ctx.showStatus(
|
|
939
|
+
overSsh
|
|
940
|
+
? `Image not found at ${displayPath}. Over SSH this path is local to your terminal — paste the image directly (clipboard image-paste shortcut) to send its bytes.`
|
|
941
|
+
: `Image not found at ${displayPath}`,
|
|
942
|
+
);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
876
945
|
this.ctx.editor.pasteText(path);
|
|
877
946
|
this.ctx.ui.requestRender();
|
|
878
|
-
this.ctx.showStatus(
|
|
879
|
-
error instanceof ImageInputTooLargeError ? error.message : "Failed to read pasted image path",
|
|
880
|
-
);
|
|
947
|
+
this.ctx.showStatus("Failed to read pasted image path");
|
|
881
948
|
}
|
|
882
949
|
}
|
|
883
950
|
|
|
@@ -1006,7 +1073,7 @@ export class InputController {
|
|
|
1006
1073
|
// the cycle status is just a status-line-style chip track (active role
|
|
1007
1074
|
// filled), matching the plan-approval model slider.
|
|
1008
1075
|
const track = renderSegmentTrack(
|
|
1009
|
-
cycleOrder.map(role => ({ label: role
|
|
1076
|
+
cycleOrder.map(role => ({ label: role })),
|
|
1010
1077
|
cycleOrder.indexOf(result.role),
|
|
1011
1078
|
);
|
|
1012
1079
|
this.ctx.showStatus(track, { dim: false });
|
|
@@ -1039,18 +1106,19 @@ export class InputController {
|
|
|
1039
1106
|
|
|
1040
1107
|
toggleThinkingBlockVisibility(): void {
|
|
1041
1108
|
this.ctx.hideThinkingBlock = !this.ctx.hideThinkingBlock;
|
|
1042
|
-
settings.set("hideThinkingBlock", this.ctx.hideThinkingBlock);
|
|
1109
|
+
this.ctx.settings.set("hideThinkingBlock", this.ctx.hideThinkingBlock);
|
|
1043
1110
|
this.ctx.session.agent.hideThinkingSummary = this.ctx.hideThinkingBlock;
|
|
1044
1111
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1112
|
+
for (const child of this.ctx.chatContainer.children) {
|
|
1113
|
+
if (child instanceof AssistantMessageComponent) {
|
|
1114
|
+
child.setHideThinkingBlock(this.ctx.hideThinkingBlock);
|
|
1115
|
+
child.invalidate();
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1048
1118
|
|
|
1049
|
-
// If streaming, re-add the streaming component with updated visibility and re-render
|
|
1050
1119
|
if (this.ctx.streamingComponent && this.ctx.streamingMessage) {
|
|
1051
1120
|
this.ctx.streamingComponent.setHideThinkingBlock(this.ctx.hideThinkingBlock);
|
|
1052
1121
|
this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
|
|
1053
|
-
this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
|
|
1054
1122
|
}
|
|
1055
1123
|
|
|
1056
1124
|
this.ctx.showStatus(`Thinking blocks: ${this.ctx.hideThinkingBlock ? "hidden" : "visible"}`);
|
|
@@ -114,6 +114,9 @@ export class SelectorController {
|
|
|
114
114
|
thinkingLevel: this.ctx.session.thinkingLevel,
|
|
115
115
|
availableThemes,
|
|
116
116
|
cwd: getProjectDir(),
|
|
117
|
+
model: this.ctx.session.model,
|
|
118
|
+
imageBudget: this.ctx.ui.imageBudget,
|
|
119
|
+
requestRender: () => this.ctx.ui.requestRender(),
|
|
117
120
|
},
|
|
118
121
|
{
|
|
119
122
|
onChange: (id, value) => this.handleSettingChange(id, value),
|
|
@@ -313,10 +316,9 @@ export class SelectorController {
|
|
|
313
316
|
for (const child of this.ctx.chatContainer.children) {
|
|
314
317
|
if (child instanceof AssistantMessageComponent) {
|
|
315
318
|
child.setHideThinkingBlock(value as boolean);
|
|
319
|
+
child.invalidate();
|
|
316
320
|
}
|
|
317
321
|
}
|
|
318
|
-
this.ctx.chatContainer.clear();
|
|
319
|
-
this.ctx.rebuildChatFromMessages();
|
|
320
322
|
break;
|
|
321
323
|
case "theme": {
|
|
322
324
|
setTheme(value as string, true).then(result => {
|
|
@@ -1186,6 +1188,8 @@ export class SelectorController {
|
|
|
1186
1188
|
hubKeys,
|
|
1187
1189
|
onDone: done,
|
|
1188
1190
|
requestRender: () => this.ctx.ui.requestRender(),
|
|
1191
|
+
registry: this.ctx.collabGuest?.agentRegistry,
|
|
1192
|
+
remote: this.ctx.collabGuest?.hubRemote,
|
|
1189
1193
|
});
|
|
1190
1194
|
|
|
1191
1195
|
overlayHandle = this.ctx.ui.showOverlay(hub, {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import { getSegmenter } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import { LRUCache } from "lru-cache/raw";
|
|
3
4
|
import type { AssistantMessageComponent } from "../components/assistant-message";
|
|
4
5
|
|
|
5
6
|
export const STREAMING_REVEAL_FRAME_MS = 1000 / 30;
|
|
@@ -15,11 +16,17 @@ type StreamingRevealControllerOptions = {
|
|
|
15
16
|
requestRender(): void;
|
|
16
17
|
};
|
|
17
18
|
|
|
19
|
+
const graphemeCountCache = new LRUCache<string, number>({ max: 128 });
|
|
20
|
+
|
|
18
21
|
function countGraphemes(text: string): number {
|
|
22
|
+
if (text.length === 0) return 0;
|
|
23
|
+
const cached = graphemeCountCache.get(text);
|
|
24
|
+
if (cached !== undefined) return cached;
|
|
19
25
|
let count = 0;
|
|
20
26
|
for (const _segment of getSegmenter().segment(text)) {
|
|
21
27
|
count += 1;
|
|
22
28
|
}
|
|
29
|
+
graphemeCountCache.set(text, count);
|
|
23
30
|
return count;
|
|
24
31
|
}
|
|
25
32
|
|
|
@@ -49,8 +49,9 @@ import {
|
|
|
49
49
|
} from "@oh-my-pi/pi-utils";
|
|
50
50
|
import chalk from "chalk";
|
|
51
51
|
import { reset as resetCapabilities } from "../capability";
|
|
52
|
+
import type { CollabGuestLink } from "../collab/guest";
|
|
53
|
+
import type { CollabHost } from "../collab/host";
|
|
52
54
|
import { KeybindingsManager } from "../config/keybindings";
|
|
53
|
-
import { MODEL_ROLES, type ModelRole } from "../config/model-roles";
|
|
54
55
|
import { isSettingsInitialized, onStatusLineSessionAccentChanged, Settings, settings } from "../config/settings";
|
|
55
56
|
import { clearClaudePluginRootsCache } from "../discovery/helpers";
|
|
56
57
|
import type {
|
|
@@ -62,7 +63,7 @@ import type {
|
|
|
62
63
|
ExtensionWidgetOptions,
|
|
63
64
|
} from "../extensibility/extensions";
|
|
64
65
|
import type { CompactOptions } from "../extensibility/extensions/types";
|
|
65
|
-
import {
|
|
66
|
+
import { loadSlashCommands } from "../extensibility/slash-commands";
|
|
66
67
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
67
68
|
import { resolveLocalUrlToPath } from "../internal-urls";
|
|
68
69
|
import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events";
|
|
@@ -82,7 +83,7 @@ import { HistoryStorage } from "../session/history-storage";
|
|
|
82
83
|
import type { SessionContext, SessionManager } from "../session/session-manager";
|
|
83
84
|
import { getRecentSessions } from "../session/session-manager";
|
|
84
85
|
import type { ShakeMode } from "../session/shake-types";
|
|
85
|
-
import { BUILTIN_SLASH_COMMAND_RESERVED_NAMES } from "../slash-commands/builtin-registry";
|
|
86
|
+
import { BUILTIN_SLASH_COMMAND_RESERVED_NAMES, BUILTIN_SLASH_COMMANDS } from "../slash-commands/builtin-registry";
|
|
86
87
|
import { formatDuration } from "../slash-commands/helpers/format";
|
|
87
88
|
import { STTController, type SttState } from "../stt";
|
|
88
89
|
import { discoverTitleSystemPromptFile, resolvePromptInput } from "../system-prompt";
|
|
@@ -397,6 +398,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
397
398
|
fileSlashCommands: Set<string> = new Set();
|
|
398
399
|
skillCommands: Map<string, string> = new Map();
|
|
399
400
|
oauthManualInput: OAuthManualInputManager = new OAuthManualInputManager();
|
|
401
|
+
collabHost?: CollabHost;
|
|
402
|
+
collabGuest?: CollabGuestLink;
|
|
400
403
|
|
|
401
404
|
#pendingSlashCommands: SlashCommand[] = [];
|
|
402
405
|
#cleanupUnsubscribe?: () => void;
|
|
@@ -423,6 +426,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
423
426
|
readonly #commandController: CommandController;
|
|
424
427
|
readonly #todoCommandController: TodoCommandController;
|
|
425
428
|
readonly #eventController: EventController;
|
|
429
|
+
get eventController(): EventController {
|
|
430
|
+
return this.#eventController;
|
|
431
|
+
}
|
|
432
|
+
get eventBus(): EventBus | undefined {
|
|
433
|
+
return this.#eventBus;
|
|
434
|
+
}
|
|
426
435
|
readonly #extensionUiController: ExtensionUiController;
|
|
427
436
|
readonly #inputController: InputController;
|
|
428
437
|
readonly #selectorController: SelectorController;
|
|
@@ -1162,6 +1171,30 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1162
1171
|
// of restarting the visible conversation (the LLM context still resets).
|
|
1163
1172
|
const context = this.session.buildTranscriptSessionContext();
|
|
1164
1173
|
this.renderSessionContext(context);
|
|
1174
|
+
// During the pre-streaming window — after `startPendingSubmission` has
|
|
1175
|
+
// optimistically rendered the user's message but before the user
|
|
1176
|
+
// `message_start` event lands it in `session` entries — any rebuild
|
|
1177
|
+
// (e.g. Ctrl+T toggleThinkingBlockVisibility, theme selector) would
|
|
1178
|
+
// otherwise erase the user's just-submitted message until the first
|
|
1179
|
+
// assistant token arrived (#2372). Once `message_start` fires the
|
|
1180
|
+
// signature is cleared by `EventController`, so this replay is a no-op
|
|
1181
|
+
// post-streaming and cannot duplicate.
|
|
1182
|
+
this.#replayOptimisticUserMessage();
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
#replayOptimisticUserMessage(): void {
|
|
1186
|
+
if (!this.optimisticUserMessageSignature) return;
|
|
1187
|
+
const submission = this.#pendingSubmittedInput;
|
|
1188
|
+
if (!submission || submission.cancelled || submission.customType) return;
|
|
1189
|
+
this.addMessageToChat(
|
|
1190
|
+
{
|
|
1191
|
+
role: "user",
|
|
1192
|
+
content: [{ type: "text", text: submission.text }, ...(submission.images ?? [])],
|
|
1193
|
+
attribution: "user",
|
|
1194
|
+
timestamp: Date.now(),
|
|
1195
|
+
},
|
|
1196
|
+
{ imageLinks: submission.imageLinks },
|
|
1197
|
+
);
|
|
1165
1198
|
}
|
|
1166
1199
|
|
|
1167
1200
|
#formatTodoLine(todo: TodoItem, prefix: string, matched: boolean): string {
|
|
@@ -2467,7 +2500,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2467
2500
|
index: startTierIndex,
|
|
2468
2501
|
segments: cycle.models.map(entry => ({
|
|
2469
2502
|
label: entry.role,
|
|
2470
|
-
color: MODEL_ROLES[entry.role as ModelRole]?.color,
|
|
2471
2503
|
detail: entry.model.name || entry.model.id,
|
|
2472
2504
|
})),
|
|
2473
2505
|
onChange: index => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
|
|
1
|
+
import { type SelectItem, SelectList, type SgrMouseEvent } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import { getSelectListTheme, type SymbolPreset, setSymbolPreset, theme } from "../../theme/theme";
|
|
3
3
|
import type { SetupScene, SetupSceneController, SetupSceneHost } from "./types";
|
|
4
4
|
|
|
@@ -29,6 +29,8 @@ class GlyphSceneController implements SetupSceneController {
|
|
|
29
29
|
#selectList: SelectList;
|
|
30
30
|
#previewRequest = 0;
|
|
31
31
|
#committing = false;
|
|
32
|
+
/** Render line where the select list begins. */
|
|
33
|
+
#listRowStart = 0;
|
|
32
34
|
|
|
33
35
|
constructor(private readonly host: SetupSceneHost) {
|
|
34
36
|
this.#selectList = new SelectList(GLYPH_ITEMS, GLYPH_ITEMS.length, getSelectListTheme());
|
|
@@ -60,12 +62,28 @@ class GlyphSceneController implements SetupSceneController {
|
|
|
60
62
|
this.#selectList.handleInput(data);
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
/** Wheel moves the highlight (live preview); hover lights the row under the pointer; click confirms it. */
|
|
66
|
+
routeMouse(event: SgrMouseEvent, line: number, _col: number): void {
|
|
67
|
+
if (this.#committing) return;
|
|
68
|
+
if (event.wheel !== null) {
|
|
69
|
+
this.#selectList.handleWheel(event.wheel);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const index = this.#selectList.hitTest(line - this.#listRowStart);
|
|
73
|
+
if (event.motion) {
|
|
74
|
+
this.#selectList.setHoverIndex(index ?? null);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (event.leftClick && index !== undefined) {
|
|
78
|
+
this.#selectList.clickItem(index);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
63
82
|
render(width: number): readonly string[] {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
];
|
|
83
|
+
const lines = [theme.fg("muted", "If a row shows boxes, tofu, or misaligned icons, pick another."), ""];
|
|
84
|
+
this.#listRowStart = lines.length;
|
|
85
|
+
lines.push(...this.#selectList.render(width));
|
|
86
|
+
return lines;
|
|
69
87
|
}
|
|
70
88
|
|
|
71
89
|
async #commit(preset: SymbolPreset): Promise<void> {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TabBar } from "@oh-my-pi/pi-tui";
|
|
1
|
+
import { type SgrMouseEvent, TabBar } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import { getTabBarTheme } from "../../shared";
|
|
3
3
|
import { SignInTab } from "./sign-in";
|
|
4
4
|
import type { SetupScene, SetupSceneController, SetupSceneHost, SetupTab } from "./types";
|
|
@@ -16,6 +16,8 @@ class ProvidersSceneController implements SetupSceneController {
|
|
|
16
16
|
|
|
17
17
|
#tabs: SetupTab[];
|
|
18
18
|
#tabBar: TabBar;
|
|
19
|
+
/** Lines the tab bar occupied in the last render (body starts one blank line below). */
|
|
20
|
+
#tabRowCount = 1;
|
|
19
21
|
|
|
20
22
|
constructor(host: SetupSceneHost) {
|
|
21
23
|
this.#tabs = [new SignInTab(host), new WebSearchTab(host)];
|
|
@@ -52,8 +54,40 @@ class ProvidersSceneController implements SetupSceneController {
|
|
|
52
54
|
tab.handleInput(data);
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Hit-test mouse reports against the last render: rows inside the tab bar
|
|
59
|
+
* hover/switch tabs (suppressed while the active panel is modal, matching
|
|
60
|
+
* keyboard tab cycling); everything else forwards to the active panel at
|
|
61
|
+
* panel-local coordinates. Wheel always goes to the panel so scrolling
|
|
62
|
+
* works regardless of pointer position.
|
|
63
|
+
*/
|
|
64
|
+
routeMouse(event: SgrMouseEvent, line: number, col: number): void {
|
|
65
|
+
const tab = this.#activeTab();
|
|
66
|
+
if (event.wheel === null && line >= 0 && line < this.#tabRowCount) {
|
|
67
|
+
if (tab.modal) return;
|
|
68
|
+
const hit = this.#tabBar.tabAt(line, col);
|
|
69
|
+
if (event.motion) {
|
|
70
|
+
this.#tabBar.setHoverTab(hit && !hit.muted ? hit.id : null);
|
|
71
|
+
} else if (event.leftClick && hit) {
|
|
72
|
+
this.#tabBar.selectTab(hit.id);
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (event.motion) this.#tabBar.setHoverTab(null);
|
|
77
|
+
const bodyLine = line - this.#tabRowCount - 1;
|
|
78
|
+
if (tab.routeMouse) {
|
|
79
|
+
tab.routeMouse(event, bodyLine, col);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (event.wheel !== null && !tab.modal) {
|
|
83
|
+
tab.handleInput(event.wheel === -1 ? "\x1b[A" : "\x1b[B");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
55
87
|
render(width: number): readonly string[] {
|
|
56
|
-
|
|
88
|
+
const tabLines = this.#tabBar.render(width);
|
|
89
|
+
this.#tabRowCount = tabLines.length;
|
|
90
|
+
return [...tabLines, "", ...this.#activeTab().render(width)];
|
|
57
91
|
}
|
|
58
92
|
|
|
59
93
|
dispose(): void {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import { PASTE_CODE_LOGIN_PROVIDERS } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import type { OAuthProvider } from "@oh-my-pi/pi-ai/oauth/types";
|
|
4
|
-
import { Input, matchesKey, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
|
|
4
|
+
import { Input, matchesKey, type SgrMouseEvent, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
|
|
5
5
|
import { getAgentDbPath } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import { OAuthSelectorComponent } from "../../components/oauth-selector";
|
|
7
7
|
import { theme } from "../../theme/theme";
|
|
@@ -35,6 +35,8 @@ export class SignInTab implements SetupTab {
|
|
|
35
35
|
#loginAbort: AbortController | undefined;
|
|
36
36
|
#loggingInProvider: string | undefined;
|
|
37
37
|
#disposed = false;
|
|
38
|
+
/** Render line where the provider selector begins. */
|
|
39
|
+
#selectorRowStart = 2;
|
|
38
40
|
|
|
39
41
|
constructor(private readonly host: SetupSceneHost) {
|
|
40
42
|
this.#authStorage = host.ctx.session.modelRegistry.authStorage;
|
|
@@ -68,12 +70,19 @@ export class SignInTab implements SetupTab {
|
|
|
68
70
|
this.#selector.handleInput(data);
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
/** Forward mouse to the provider selector; pointer is inert during an active login or code prompt. */
|
|
74
|
+
routeMouse(event: SgrMouseEvent, line: number, col: number): void {
|
|
75
|
+
if (this.#loggingInProvider || this.#prompt) return;
|
|
76
|
+
this.#selector.routeMouse(event, line - this.#selectorRowStart, col);
|
|
77
|
+
}
|
|
78
|
+
|
|
71
79
|
render(width: number): readonly string[] {
|
|
72
80
|
const lines: string[] = [];
|
|
73
81
|
if (this.#loggingInProvider) {
|
|
74
82
|
lines.push(theme.bold(`Signing in to ${this.#loggingInProvider}`));
|
|
75
83
|
} else {
|
|
76
84
|
lines.push(theme.fg("muted", "Pick a provider to sign in — you can connect more than one."), "");
|
|
85
|
+
this.#selectorRowStart = lines.length;
|
|
77
86
|
lines.push(...this.#selector.render(width));
|
|
78
87
|
}
|
|
79
88
|
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
padding,
|
|
3
|
+
type SelectItem,
|
|
4
|
+
SelectList,
|
|
5
|
+
type SgrMouseEvent,
|
|
6
|
+
truncateToWidth,
|
|
7
|
+
visibleWidth,
|
|
8
|
+
} from "@oh-my-pi/pi-tui";
|
|
2
9
|
import {
|
|
3
10
|
enableAutoTheme,
|
|
4
11
|
getAvailableThemes,
|
|
@@ -89,6 +96,8 @@ class ThemeSceneController implements SetupSceneController {
|
|
|
89
96
|
#message: string | undefined;
|
|
90
97
|
#previewRequest = 0;
|
|
91
98
|
#disposed = false;
|
|
99
|
+
/** Render line where the select list began, or -1 while it is not shown. */
|
|
100
|
+
#listRowStart = -1;
|
|
92
101
|
readonly #originalTheme = getCurrentThemeName();
|
|
93
102
|
readonly #originalSymbolPreset: SymbolPreset;
|
|
94
103
|
readonly #originalColorBlindMode: boolean;
|
|
@@ -117,6 +126,22 @@ class ThemeSceneController implements SetupSceneController {
|
|
|
117
126
|
this.#selectList.handleInput(data);
|
|
118
127
|
}
|
|
119
128
|
|
|
129
|
+
/** Wheel moves the highlight (live preview); hover lights the row under the pointer; click confirms it. */
|
|
130
|
+
routeMouse(event: SgrMouseEvent, line: number, _col: number): void {
|
|
131
|
+
if (event.wheel !== null) {
|
|
132
|
+
this.#selectList.handleWheel(event.wheel);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const index = this.#listRowStart >= 0 ? this.#selectList.hitTest(line - this.#listRowStart) : undefined;
|
|
136
|
+
if (event.motion) {
|
|
137
|
+
this.#selectList.setHoverIndex(index ?? null);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (event.leftClick && index !== undefined) {
|
|
141
|
+
this.#selectList.clickItem(index);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
120
145
|
render(width: number): readonly string[] {
|
|
121
146
|
const lines = [
|
|
122
147
|
theme.fg("muted", "Theme changes preview live. Nothing is saved until you press Enter."),
|
|
@@ -128,8 +153,10 @@ class ThemeSceneController implements SetupSceneController {
|
|
|
128
153
|
"",
|
|
129
154
|
];
|
|
130
155
|
if (this.#loadingAllThemes) {
|
|
156
|
+
this.#listRowStart = -1;
|
|
131
157
|
lines.push(theme.fg("dim", "Loading themes…"));
|
|
132
158
|
} else {
|
|
159
|
+
this.#listRowStart = lines.length;
|
|
133
160
|
lines.push(...this.#selectList.render(width));
|
|
134
161
|
}
|
|
135
162
|
if (this.#message) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Component } from "@oh-my-pi/pi-tui";
|
|
1
|
+
import type { Component, SgrMouseEvent } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import type { InteractiveModeContext } from "../../types";
|
|
3
3
|
|
|
4
4
|
export type SetupSceneResult = "done" | "skipped";
|
|
@@ -17,6 +17,13 @@ export interface SetupSceneController extends Component {
|
|
|
17
17
|
onMount?(): void | Promise<void>;
|
|
18
18
|
onUnmount?(): void;
|
|
19
19
|
dispose?(): void;
|
|
20
|
+
/**
|
|
21
|
+
* Route an SGR mouse report (tracking is on while the wizard holds the
|
|
22
|
+
* alternate screen). `line`/`col` are 0-based within this controller's
|
|
23
|
+
* last rendered output. When absent, the wizard falls back to synthesizing
|
|
24
|
+
* arrow keys from wheel notches.
|
|
25
|
+
*/
|
|
26
|
+
routeMouse?(event: SgrMouseEvent, line: number, col: number): void;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
/**
|
|
@@ -36,6 +43,8 @@ export interface SetupTab {
|
|
|
36
43
|
invalidate(): void;
|
|
37
44
|
/** Called when the tab becomes active (including initial mount). */
|
|
38
45
|
onActivate?(): void;
|
|
46
|
+
/** Mouse routing at tab-local coordinates; see {@link SetupSceneController.routeMouse}. */
|
|
47
|
+
routeMouse?(event: SgrMouseEvent, line: number, col: number): void;
|
|
39
48
|
dispose(): void;
|
|
40
49
|
}
|
|
41
50
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SelectItem, SelectList, truncateToWidth } from "@oh-my-pi/pi-tui";
|
|
1
|
+
import { type SelectItem, SelectList, type SgrMouseEvent, truncateToWidth } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import { SETTINGS_SCHEMA } from "../../../config/settings-schema";
|
|
3
3
|
import { getSearchProvider, setPreferredSearchProvider } from "../../../web/search/provider";
|
|
4
4
|
import { isSearchProviderPreference, type SearchProviderId } from "../../../web/search/types";
|
|
@@ -31,6 +31,8 @@ export class WebSearchTab implements SetupTab {
|
|
|
31
31
|
#availability = new Map<SearchProviderId, Availability>();
|
|
32
32
|
#status: string[] = [];
|
|
33
33
|
#disposed = false;
|
|
34
|
+
/** Render line where the select list begins. */
|
|
35
|
+
#listRowStart = 0;
|
|
34
36
|
|
|
35
37
|
constructor(private readonly host: SetupSceneHost) {
|
|
36
38
|
this.#list = new SelectList(WEB_SEARCH_ITEMS, MAX_VISIBLE, getSelectListTheme());
|
|
@@ -55,6 +57,22 @@ export class WebSearchTab implements SetupTab {
|
|
|
55
57
|
this.#list.handleInput(data);
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
/** Wheel moves the highlight; hover lights the row under the pointer; click confirms it. */
|
|
61
|
+
routeMouse(event: SgrMouseEvent, line: number, _col: number): void {
|
|
62
|
+
if (event.wheel !== null) {
|
|
63
|
+
this.#list.handleWheel(event.wheel);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const index = this.#list.hitTest(line - this.#listRowStart);
|
|
67
|
+
if (event.motion) {
|
|
68
|
+
this.#list.setHoverIndex(index ?? null);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (event.leftClick && index !== undefined) {
|
|
72
|
+
this.#list.clickItem(index);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
58
76
|
invalidate(): void {
|
|
59
77
|
this.#list.invalidate();
|
|
60
78
|
}
|
|
@@ -64,11 +82,9 @@ export class WebSearchTab implements SetupTab {
|
|
|
64
82
|
}
|
|
65
83
|
|
|
66
84
|
render(width: number): readonly string[] {
|
|
67
|
-
const lines = [
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
...this.#list.render(width),
|
|
71
|
-
];
|
|
85
|
+
const lines = [theme.fg("muted", "Choose the provider the web_search tool should prefer."), ""];
|
|
86
|
+
this.#listRowStart = lines.length;
|
|
87
|
+
lines.push(...this.#list.render(width));
|
|
72
88
|
const selected = this.#list.getSelectedItem();
|
|
73
89
|
if (selected) {
|
|
74
90
|
lines.push("", ...this.#readinessLines(selected.value).map(line => truncateToWidth(line, width)));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Component, matchesKey, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
1
|
+
import { type Component, matchesKey, padding, parseSgrMouse, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import { APP_NAME } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import { gradientLogo, PI_LOGO } from "../components/welcome";
|
|
4
4
|
import { theme } from "../theme/theme";
|
|
@@ -61,6 +61,8 @@ export class SetupWizardComponent implements Component {
|
|
|
61
61
|
#timer: NodeJS.Timeout | undefined;
|
|
62
62
|
#done = Promise.withResolvers<void>();
|
|
63
63
|
#disposed = false;
|
|
64
|
+
/** Screen row where the active scene's body began in the last rendered frame. */
|
|
65
|
+
#bodyRowStart = 0;
|
|
64
66
|
|
|
65
67
|
constructor(
|
|
66
68
|
readonly ctx: InteractiveModeContext,
|
|
@@ -87,6 +89,10 @@ export class SetupWizardComponent implements Component {
|
|
|
87
89
|
|
|
88
90
|
handleInput(data: string): void {
|
|
89
91
|
if (this.#phase === "done") return;
|
|
92
|
+
if (data.startsWith("\x1b[<")) {
|
|
93
|
+
this.#handleMouse(data);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
90
96
|
if (matchesKey(data, "ctrl+c")) {
|
|
91
97
|
this.#beginOutro();
|
|
92
98
|
return;
|
|
@@ -116,6 +122,36 @@ export class SetupWizardComponent implements Component {
|
|
|
116
122
|
this.#activeScene?.handleInput?.(data);
|
|
117
123
|
}
|
|
118
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Mouse handling for the fullscreen wizard (SGR tracking is on while the
|
|
127
|
+
* overlay holds the alternate screen). The frame paints from screen row 0,
|
|
128
|
+
* so report coordinates index directly into the last rendered lines: scene
|
|
129
|
+
* body rows start at #bodyRowStart, indented by SCENE_MARGIN_X. Scenes
|
|
130
|
+
* that implement routeMouse get hit-tested events (wheel, hover, click);
|
|
131
|
+
* for the rest a wheel notch falls back to an arrow key. A left click
|
|
132
|
+
* advances the splash/outro like Enter. Raw reports never reach scene
|
|
133
|
+
* keyboard input.
|
|
134
|
+
*/
|
|
135
|
+
#handleMouse(data: string): void {
|
|
136
|
+
const event = parseSgrMouse(data);
|
|
137
|
+
if (!event) return;
|
|
138
|
+
if (this.#phase === "splash" || this.#phase === "outro") {
|
|
139
|
+
if (!event.leftClick) return;
|
|
140
|
+
if (this.#phase === "splash") this.#beginScene();
|
|
141
|
+
else this.#complete();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const scene = this.#activeScene;
|
|
145
|
+
if (!scene) return;
|
|
146
|
+
if (scene.routeMouse) {
|
|
147
|
+
scene.routeMouse(event, event.row - this.#bodyRowStart, event.col - SCENE_MARGIN_X);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (event.wheel !== null) {
|
|
151
|
+
scene.handleInput?.(event.wheel === -1 ? "\x1b[A" : "\x1b[B");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
119
155
|
render(width: number): readonly string[] {
|
|
120
156
|
const safeWidth = Math.max(1, width);
|
|
121
157
|
const height = Math.max(1, this.ctx.ui.terminal.rows);
|
|
@@ -163,6 +199,7 @@ export class SetupWizardComponent implements Component {
|
|
|
163
199
|
header.push(indentLine(theme.fg("muted", subtitle), width, SCENE_MARGIN_X));
|
|
164
200
|
}
|
|
165
201
|
header.push("");
|
|
202
|
+
this.#bodyRowStart = header.length;
|
|
166
203
|
|
|
167
204
|
const footer = [
|
|
168
205
|
"",
|