@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.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 +75 -1
- package/dist/types/cli/dry-balance-cli.d.ts +15 -1
- package/dist/types/commit/analysis/conventional.d.ts +2 -2
- package/dist/types/commit/analysis/summary.d.ts +2 -2
- package/dist/types/commit/changelog/generate.d.ts +2 -2
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/index.d.ts +3 -3
- package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
- package/dist/types/commit/model-selection.d.ts +10 -4
- package/dist/types/config/api-key-resolver.d.ts +34 -0
- package/dist/types/config/model-registry.d.ts +17 -1
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/dap/config.d.ts +14 -1
- package/dist/types/dap/types.d.ts +10 -0
- package/dist/types/lsp/utils.d.ts +3 -2
- package/dist/types/modes/components/chat-block.d.ts +64 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -0
- package/dist/types/modes/components/overlay-box.d.ts +17 -0
- package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
- package/dist/types/modes/components/plan-toc.d.ts +41 -0
- package/dist/types/modes/components/read-tool-group.d.ts +2 -0
- package/dist/types/modes/components/transcript-container.d.ts +11 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
- package/dist/types/modes/controllers/input-controller.d.ts +1 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
- package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +15 -5
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +18 -5
- package/dist/types/modes/utils/copy-targets.d.ts +21 -1
- package/dist/types/plan-mode/approved-plan.d.ts +27 -8
- package/dist/types/plan-mode/plan-protection.d.ts +4 -4
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +21 -0
- package/dist/types/session/messages.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/slash-commands/types.d.ts +4 -6
- package/dist/types/task/executor.d.ts +7 -0
- package/dist/types/task/index.d.ts +1 -0
- package/dist/types/task/render.d.ts +3 -2
- package/dist/types/tools/archive-reader.d.ts +5 -0
- package/dist/types/tools/ast-edit.d.ts +3 -0
- package/dist/types/tools/ast-grep.d.ts +3 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/find.d.ts +8 -4
- package/dist/types/tools/grouped-file-output.d.ts +95 -12
- package/dist/types/tools/memory-render.d.ts +4 -1
- package/dist/types/tools/plan-mode-guard.d.ts +8 -9
- package/dist/types/tools/render-utils.d.ts +5 -9
- package/dist/types/tools/search.d.ts +4 -0
- package/dist/types/tools/sqlite-reader.d.ts +1 -0
- package/dist/types/tools/todo.d.ts +3 -2
- package/dist/types/tools/write.d.ts +3 -0
- package/dist/types/tui/output-block.d.ts +16 -4
- package/dist/types/tui/status-line.d.ts +3 -0
- package/dist/types/utils/enhanced-paste.d.ts +20 -0
- package/dist/types/web/search/providers/kimi.d.ts +1 -1
- package/package.json +9 -9
- package/src/auto-thinking/classifier.ts +5 -1
- package/src/cli/dry-balance-cli.ts +52 -17
- package/src/cli/gallery-cli.ts +4 -1
- package/src/cli/gallery-fixtures/misc.ts +29 -0
- package/src/commit/analysis/conventional.ts +2 -2
- package/src/commit/analysis/summary.ts +2 -2
- package/src/commit/changelog/generate.ts +2 -2
- package/src/commit/changelog/index.ts +2 -2
- package/src/commit/map-reduce/index.ts +3 -3
- package/src/commit/map-reduce/map-phase.ts +2 -2
- package/src/commit/map-reduce/reduce-phase.ts +2 -2
- package/src/commit/model-selection.ts +33 -9
- package/src/commit/pipeline.ts +4 -4
- package/src/config/api-key-resolver.ts +58 -0
- package/src/config/model-registry.ts +25 -2
- package/src/config/settings-schema.ts +10 -0
- package/src/config/settings.ts +20 -2
- package/src/dap/config.ts +41 -2
- package/src/dap/defaults.json +1 -0
- package/src/dap/session.ts +1 -0
- package/src/dap/types.ts +10 -0
- package/src/debug/index.ts +40 -54
- package/src/edit/renderer.ts +82 -78
- package/src/eval/__tests__/llm-bridge.test.ts +90 -31
- package/src/eval/llm-bridge.ts +8 -3
- package/src/goals/tools/goal-tool.ts +36 -26
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/lsp/utils.ts +3 -2
- package/src/main.ts +9 -7
- package/src/memories/index.ts +12 -5
- package/src/mnemopi/backend.ts +5 -1
- package/src/modes/acp/acp-agent.ts +33 -26
- package/src/modes/components/assistant-message.ts +2 -9
- package/src/modes/components/chat-block.ts +111 -0
- package/src/modes/components/copy-selector.ts +1 -44
- package/src/modes/components/custom-editor.ts +23 -0
- package/src/modes/components/custom-message.ts +1 -3
- package/src/modes/components/execution-shared.ts +1 -2
- package/src/modes/components/hook-message.ts +1 -3
- package/src/modes/components/overlay-box.ts +108 -0
- package/src/modes/components/plan-review-overlay.ts +799 -0
- package/src/modes/components/plan-toc.ts +138 -0
- package/src/modes/components/read-tool-group.ts +20 -4
- package/src/modes/components/skill-message.ts +0 -1
- package/src/modes/components/tips.txt +1 -0
- package/src/modes/components/todo-reminder.ts +0 -2
- package/src/modes/components/tool-execution.ts +68 -88
- package/src/modes/components/transcript-container.ts +84 -24
- package/src/modes/components/user-message.ts +1 -2
- package/src/modes/controllers/command-controller-shared.ts +7 -6
- package/src/modes/controllers/command-controller.ts +57 -55
- package/src/modes/controllers/event-controller.ts +41 -40
- package/src/modes/controllers/extension-ui-controller.ts +10 -73
- package/src/modes/controllers/input-controller.ts +124 -119
- package/src/modes/controllers/mcp-command-controller.ts +69 -60
- package/src/modes/controllers/selector-controller.ts +23 -25
- package/src/modes/controllers/streaming-reveal.ts +212 -0
- package/src/modes/controllers/tan-command-controller.ts +173 -0
- package/src/modes/interactive-mode.ts +169 -94
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/theme/theme-schema.json +1 -1
- package/src/modes/theme/theme.ts +8 -4
- package/src/modes/types.ts +18 -7
- package/src/modes/utils/copy-targets.ts +133 -27
- package/src/modes/utils/ui-helpers.ts +44 -46
- package/src/plan-mode/approved-plan.ts +66 -43
- package/src/plan-mode/plan-protection.ts +4 -4
- package/src/prompts/system/background-tan-dispatch.md +8 -0
- package/src/prompts/system/plan-mode-active.md +67 -58
- package/src/prompts/system/plan-mode-approved.md +1 -1
- package/src/sdk.ts +11 -37
- package/src/session/agent-session.ts +82 -6
- package/src/session/messages.ts +26 -0
- package/src/session/session-manager.ts +13 -5
- package/src/slash-commands/builtin-registry.ts +36 -9
- package/src/slash-commands/types.ts +4 -6
- package/src/task/executor.ts +5 -2
- package/src/task/index.ts +4 -0
- package/src/task/render.ts +212 -147
- package/src/tools/archive-reader.ts +64 -0
- package/src/tools/ask.ts +119 -164
- package/src/tools/ast-edit.ts +98 -71
- package/src/tools/ast-grep.ts +37 -43
- package/src/tools/bash.ts +50 -6
- package/src/tools/debug.ts +20 -8
- package/src/tools/fetch.ts +297 -7
- package/src/tools/find.ts +44 -30
- package/src/tools/gh-renderer.ts +81 -42
- package/src/tools/grouped-file-output.ts +272 -48
- package/src/tools/image-gen.ts +150 -103
- package/src/tools/inspect-image-renderer.ts +63 -41
- package/src/tools/inspect-image.ts +8 -1
- package/src/tools/job.ts +3 -4
- package/src/tools/memory-render.ts +4 -1
- package/src/tools/plan-mode-guard.ts +21 -39
- package/src/tools/read.ts +23 -16
- package/src/tools/render-utils.ts +21 -37
- package/src/tools/resolve.ts +14 -0
- package/src/tools/search-tool-bm25.ts +36 -23
- package/src/tools/search.ts +80 -78
- package/src/tools/sqlite-reader.ts +9 -12
- package/src/tools/todo.ts +118 -52
- package/src/tools/write.ts +81 -62
- package/src/tui/output-block.ts +60 -13
- package/src/tui/status-line.ts +5 -1
- package/src/utils/commit-message-generator.ts +9 -1
- package/src/utils/enhanced-paste.ts +202 -0
- package/src/utils/title-generator.ts +2 -1
- package/src/web/search/providers/anthropic.ts +25 -19
- package/src/web/search/providers/exa.ts +11 -3
- package/src/web/search/providers/kimi.ts +28 -17
- package/src/web/search/providers/parallel.ts +35 -24
- package/src/web/search/providers/synthetic.ts +8 -6
- package/src/web/search/providers/tavily.ts +9 -8
- package/src/web/search/providers/zai.ts +8 -6
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Component, OverlayHandle, TUI } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import { Container, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
3
|
-
import { logger } from "@oh-my-pi/pi-utils";
|
|
4
3
|
import { KeybindingsManager } from "../../config/keybindings";
|
|
5
4
|
import type {
|
|
6
5
|
CompactOptions,
|
|
@@ -176,10 +175,10 @@ export class ExtensionUiController {
|
|
|
176
175
|
this.ctx.streamingMessage = undefined;
|
|
177
176
|
this.ctx.pendingTools.clear();
|
|
178
177
|
|
|
179
|
-
this.ctx.
|
|
180
|
-
|
|
178
|
+
this.ctx.present([
|
|
179
|
+
new Spacer(1),
|
|
181
180
|
new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
|
|
182
|
-
);
|
|
181
|
+
]);
|
|
183
182
|
await this.ctx.reloadTodos();
|
|
184
183
|
this.ctx.ui.requestRender(true, { clearScrollback: true });
|
|
185
184
|
|
|
@@ -326,10 +325,6 @@ export class ExtensionUiController {
|
|
|
326
325
|
.then(() => this.#applyCustomMessageDisplay(wasStreaming, message.display))
|
|
327
326
|
.catch((err: unknown) => {
|
|
328
327
|
const errorText = `Extension sendMessage failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
329
|
-
if (this.ctx.isBackgrounded) {
|
|
330
|
-
logger.error(errorText);
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
328
|
this.ctx.showError(errorText);
|
|
334
329
|
});
|
|
335
330
|
},
|
|
@@ -374,9 +369,6 @@ export class ExtensionUiController {
|
|
|
374
369
|
getContextUsage: () => this.ctx.session.getContextUsage(),
|
|
375
370
|
waitForIdle: () => this.ctx.session.agent.waitForIdle(),
|
|
376
371
|
reload: async () => {
|
|
377
|
-
if (this.ctx.isBackgrounded) {
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
372
|
await this.ctx.session.reload();
|
|
381
373
|
this.ctx.chatContainer.clear();
|
|
382
374
|
this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
|
|
@@ -384,9 +376,6 @@ export class ExtensionUiController {
|
|
|
384
376
|
this.ctx.showStatus("Reloaded session");
|
|
385
377
|
},
|
|
386
378
|
newSession: async options => {
|
|
387
|
-
if (this.ctx.isBackgrounded) {
|
|
388
|
-
return { cancelled: true };
|
|
389
|
-
}
|
|
390
379
|
// Stop any loading animation
|
|
391
380
|
if (this.ctx.loadingAnimation) {
|
|
392
381
|
this.ctx.loadingAnimation.stop();
|
|
@@ -415,19 +404,16 @@ export class ExtensionUiController {
|
|
|
415
404
|
this.ctx.streamingMessage = undefined;
|
|
416
405
|
this.ctx.pendingTools.clear();
|
|
417
406
|
|
|
418
|
-
this.ctx.
|
|
419
|
-
|
|
407
|
+
this.ctx.present([
|
|
408
|
+
new Spacer(1),
|
|
420
409
|
new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
|
|
421
|
-
);
|
|
410
|
+
]);
|
|
422
411
|
await this.ctx.reloadTodos();
|
|
423
412
|
this.ctx.ui.requestRender(true, { clearScrollback: true });
|
|
424
413
|
|
|
425
414
|
return { cancelled: false };
|
|
426
415
|
},
|
|
427
416
|
branch: async entryId => {
|
|
428
|
-
if (this.ctx.isBackgrounded) {
|
|
429
|
-
return { cancelled: true };
|
|
430
|
-
}
|
|
431
417
|
const result = await this.ctx.session.branch(entryId);
|
|
432
418
|
if (result.cancelled) {
|
|
433
419
|
return { cancelled: true };
|
|
@@ -443,9 +429,6 @@ export class ExtensionUiController {
|
|
|
443
429
|
return { cancelled: false };
|
|
444
430
|
},
|
|
445
431
|
navigateTree: async (targetId, options) => {
|
|
446
|
-
if (this.ctx.isBackgrounded) {
|
|
447
|
-
return { cancelled: true };
|
|
448
|
-
}
|
|
449
432
|
const result = await this.ctx.session.navigateTree(targetId, { summarize: options?.summarize });
|
|
450
433
|
if (result.cancelled) {
|
|
451
434
|
return { cancelled: true };
|
|
@@ -464,9 +447,6 @@ export class ExtensionUiController {
|
|
|
464
447
|
},
|
|
465
448
|
compact: async instructionsOrOptions => this.#handleInteractiveCompact(instructionsOrOptions),
|
|
466
449
|
switchSession: async sessionPath => {
|
|
467
|
-
if (this.ctx.isBackgrounded) {
|
|
468
|
-
return { cancelled: true };
|
|
469
|
-
}
|
|
470
450
|
this.clearHookWidgets();
|
|
471
451
|
const result = await this.ctx.session.switchSession(sessionPath);
|
|
472
452
|
if (!result) {
|
|
@@ -482,36 +462,6 @@ export class ExtensionUiController {
|
|
|
482
462
|
extensionRunner.initialize(actions, contextActions, commandActions, uiContext);
|
|
483
463
|
}
|
|
484
464
|
|
|
485
|
-
createBackgroundUiContext(): ExtensionUIContext {
|
|
486
|
-
return {
|
|
487
|
-
select: async (_title: string, _options: ExtensionUISelectItem[], _dialogOptions) => undefined,
|
|
488
|
-
confirm: async (_title: string, _message: string, _dialogOptions) => false,
|
|
489
|
-
input: async (_title: string, _placeholder?: string, _dialogOptions?: unknown) => undefined,
|
|
490
|
-
notify: () => {},
|
|
491
|
-
onTerminalInput: () => () => {},
|
|
492
|
-
setStatus: () => {},
|
|
493
|
-
setWorkingMessage: () => {},
|
|
494
|
-
setWidget: () => {},
|
|
495
|
-
setTitle: () => {},
|
|
496
|
-
custom: async () => undefined as never,
|
|
497
|
-
setEditorText: () => {},
|
|
498
|
-
pasteToEditor: () => {},
|
|
499
|
-
getEditorText: () => "",
|
|
500
|
-
editor: async () => undefined,
|
|
501
|
-
get theme() {
|
|
502
|
-
return theme;
|
|
503
|
-
},
|
|
504
|
-
getAllThemes: () => Promise.resolve([]),
|
|
505
|
-
getTheme: () => Promise.resolve(undefined),
|
|
506
|
-
setTheme: () => Promise.resolve({ success: false, error: "Background mode" }),
|
|
507
|
-
setFooter: () => {},
|
|
508
|
-
setHeader: () => {},
|
|
509
|
-
setEditorComponent: () => {},
|
|
510
|
-
getToolsExpanded: () => false,
|
|
511
|
-
setToolsExpanded: () => {},
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
|
|
515
465
|
/**
|
|
516
466
|
* Emit session event to all extension tools.
|
|
517
467
|
*/
|
|
@@ -531,7 +481,7 @@ export class ExtensionUiController {
|
|
|
531
481
|
ui: uiContext,
|
|
532
482
|
getContextUsage: () => this.ctx.session.getContextUsage(),
|
|
533
483
|
compact: instructionsOrOptions => this.#compactSession(instructionsOrOptions),
|
|
534
|
-
hasUI:
|
|
484
|
+
hasUI: true,
|
|
535
485
|
cwd: this.ctx.sessionManager.getCwd(),
|
|
536
486
|
sessionManager: this.ctx.session.sessionManager,
|
|
537
487
|
modelRegistry: this.ctx.session.modelRegistry,
|
|
@@ -557,22 +507,14 @@ export class ExtensionUiController {
|
|
|
557
507
|
* Show a tool error in the chat.
|
|
558
508
|
*/
|
|
559
509
|
showToolError(toolName: string, error: string): void {
|
|
560
|
-
if (this.ctx.isBackgrounded) {
|
|
561
|
-
logger.error(`Tool "${toolName}" error: ${error}`);
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
510
|
const errorText = new Text(theme.fg("error", `Tool "${toolName}" error: ${error}`), 1, 0);
|
|
565
|
-
this.ctx.
|
|
566
|
-
this.ctx.ui.requestRender();
|
|
511
|
+
this.ctx.present(errorText);
|
|
567
512
|
}
|
|
568
513
|
|
|
569
514
|
/**
|
|
570
515
|
* Set hook status text in the footer.
|
|
571
516
|
*/
|
|
572
517
|
setHookStatus(key: string, text: string | undefined): void {
|
|
573
|
-
if (this.ctx.isBackgrounded) {
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
518
|
this.ctx.statusLine.setHookStatus(key, text);
|
|
577
519
|
this.ctx.ui.requestRender();
|
|
578
520
|
}
|
|
@@ -860,14 +802,9 @@ export class ExtensionUiController {
|
|
|
860
802
|
|
|
861
803
|
showExtensionError(extensionPath: string, error: string): void {
|
|
862
804
|
const errorText = new Text(theme.fg("error", `Extension "${extensionPath}" error: ${error}`), 1, 0);
|
|
863
|
-
this.ctx.
|
|
864
|
-
this.ctx.ui.requestRender();
|
|
805
|
+
this.ctx.present(errorText);
|
|
865
806
|
}
|
|
866
807
|
async #handleInteractiveCompact(instructionsOrOptions: string | CompactOptions | undefined): Promise<void> {
|
|
867
|
-
if (this.ctx.isBackgrounded) {
|
|
868
|
-
await this.#compactSession(instructionsOrOptions);
|
|
869
|
-
return;
|
|
870
|
-
}
|
|
871
808
|
await this.ctx.executeCompaction(instructionsOrOptions, false);
|
|
872
809
|
}
|
|
873
810
|
|
|
@@ -892,7 +829,7 @@ export class ExtensionUiController {
|
|
|
892
829
|
#applyCustomMessageDisplay(wasStreaming: boolean, shouldDisplay: boolean | undefined): void {
|
|
893
830
|
// For non-streaming cases with display=true, update UI
|
|
894
831
|
// (streaming cases update via message_end event)
|
|
895
|
-
if (!
|
|
832
|
+
if (!wasStreaming && shouldDisplay) {
|
|
896
833
|
this.ctx.rebuildChatFromMessages();
|
|
897
834
|
}
|
|
898
835
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
|
+
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
2
3
|
import type { AutocompleteProvider, SlashCommand } from "@oh-my-pi/pi-tui";
|
|
3
4
|
import { $env, logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
4
5
|
import { getRoleInfo } from "../../config/model-registry";
|
|
@@ -9,16 +10,16 @@ import { expandEmoticons } from "../../modes/emoji-autocomplete";
|
|
|
9
10
|
import { materializeImageReferenceLinks } from "../../modes/image-references";
|
|
10
11
|
import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
|
|
11
12
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
12
|
-
import type
|
|
13
|
-
import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
|
|
13
|
+
import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails, USER_INTERRUPT_LABEL } from "../../session/messages";
|
|
14
14
|
import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
|
|
15
15
|
import { isTinyTitleLocalModelKey } from "../../tiny/models";
|
|
16
16
|
import { isLowSignalTitleInput } from "../../tiny/text";
|
|
17
17
|
import { tinyTitleClient } from "../../tiny/title-client";
|
|
18
18
|
import type { TinyTitleProgressEvent } from "../../tiny/title-protocol";
|
|
19
19
|
import { copyToClipboard, readImageFromClipboard, readTextFromClipboard } from "../../utils/clipboard";
|
|
20
|
+
import { EnhancedPasteController } from "../../utils/enhanced-paste";
|
|
20
21
|
import { getEditorCommand, openInEditor } from "../../utils/external-editor";
|
|
21
|
-
import { ensureSupportedImageInput } from "../../utils/image-loading";
|
|
22
|
+
import { ensureSupportedImageInput, ImageInputTooLargeError, loadImageInput } from "../../utils/image-loading";
|
|
22
23
|
import { resizeImage } from "../../utils/image-resize";
|
|
23
24
|
import { generateSessionTitle, setSessionTerminalTitle } from "../../utils/title-generator";
|
|
24
25
|
|
|
@@ -40,8 +41,10 @@ const TINY_TITLE_PROGRESS_REVEAL_DELAY_MS = 1_000;
|
|
|
40
41
|
export class InputController {
|
|
41
42
|
constructor(private ctx: InteractiveModeContext) {}
|
|
42
43
|
|
|
44
|
+
#enhancedPaste?: EnhancedPasteController;
|
|
45
|
+
|
|
43
46
|
#showTinyTitleDownloadProgress(modelKey: string): void {
|
|
44
|
-
if (!isTinyTitleLocalModelKey(modelKey)
|
|
47
|
+
if (!isTinyTitleLocalModelKey(modelKey)) return;
|
|
45
48
|
const component = new TinyTitleDownloadProgressComponent(modelKey);
|
|
46
49
|
let added = false;
|
|
47
50
|
let disposed = false;
|
|
@@ -91,7 +94,7 @@ export class InputController {
|
|
|
91
94
|
if (this.ctx.loopModeEnabled) {
|
|
92
95
|
this.ctx.pauseLoop();
|
|
93
96
|
if (this.ctx.session.isStreaming) {
|
|
94
|
-
void this.ctx.session.abort();
|
|
97
|
+
void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
95
98
|
} else {
|
|
96
99
|
this.ctx.cancelPendingSubmission();
|
|
97
100
|
}
|
|
@@ -121,7 +124,7 @@ export class InputController {
|
|
|
121
124
|
this.ctx.isPythonMode = false;
|
|
122
125
|
this.ctx.updateEditorBorderColor();
|
|
123
126
|
} else if (this.ctx.session.isStreaming) {
|
|
124
|
-
void this.ctx.session.abort();
|
|
127
|
+
void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
125
128
|
} else if (!this.ctx.editor.getText().trim()) {
|
|
126
129
|
// Double-interrupt with empty editor triggers /tree, /branch, or nothing based on setting
|
|
127
130
|
const action = settings.get("doubleEscapeAction");
|
|
@@ -176,6 +179,7 @@ export class InputController {
|
|
|
176
179
|
this.ctx.keybindings.getKeys("app.clipboard.pasteImage"),
|
|
177
180
|
);
|
|
178
181
|
this.ctx.editor.onPasteImage = () => this.handleImagePaste();
|
|
182
|
+
this.ctx.editor.onPasteImagePath = path => void this.handleImagePathPaste(path);
|
|
179
183
|
this.ctx.editor.setActionKeys(
|
|
180
184
|
"app.clipboard.pasteTextRaw",
|
|
181
185
|
this.ctx.keybindings.getKeys("app.clipboard.pasteTextRaw"),
|
|
@@ -223,6 +227,8 @@ export class InputController {
|
|
|
223
227
|
this.ctx.editor.setCustomKeyHandler(key, () => this.ctx.showSessionObserver());
|
|
224
228
|
}
|
|
225
229
|
|
|
230
|
+
this.#setupEnhancedPaste();
|
|
231
|
+
|
|
226
232
|
this.ctx.editor.onChange = (text: string) => {
|
|
227
233
|
const wasBashMode = this.ctx.isBashMode;
|
|
228
234
|
const wasPythonMode = this.ctx.isPythonMode;
|
|
@@ -235,16 +241,45 @@ export class InputController {
|
|
|
235
241
|
};
|
|
236
242
|
}
|
|
237
243
|
|
|
244
|
+
#setupEnhancedPaste(): void {
|
|
245
|
+
if (this.#enhancedPaste) return;
|
|
246
|
+
|
|
247
|
+
this.#enhancedPaste = new EnhancedPasteController({
|
|
248
|
+
write: data => this.ctx.ui.terminal.write(data),
|
|
249
|
+
pasteText: text => {
|
|
250
|
+
this.ctx.editor.pasteText(text);
|
|
251
|
+
this.ctx.ui.requestRender(false, { allowUnknownViewportMutation: true });
|
|
252
|
+
},
|
|
253
|
+
pasteImage: async image => {
|
|
254
|
+
await this.#normalizeAndInsertPastedImage(image, `Unsupported pasted image format: ${image.mimeType}`);
|
|
255
|
+
},
|
|
256
|
+
showStatus: message => this.ctx.showStatus(message),
|
|
257
|
+
});
|
|
258
|
+
this.ctx.ui.addInputListener(data => (this.#enhancedPaste?.handleInput(data) ? { consume: true } : undefined));
|
|
259
|
+
this.ctx.ui.addStartListener(() => this.#enhancedPaste?.enable());
|
|
260
|
+
}
|
|
261
|
+
|
|
238
262
|
setupEditorSubmitHandler(): void {
|
|
239
263
|
this.ctx.editor.onSubmit = async (text: string) => {
|
|
240
264
|
text = text.trim();
|
|
241
265
|
if ((!isSettingsInitialized() || settings.get("emojiAutocomplete")) && text) text = expandEmoticons(text);
|
|
242
266
|
|
|
243
|
-
// Empty submit while streaming with queued
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
267
|
+
// Empty submit while streaming with queued steering: interrupt now and
|
|
268
|
+
// immediately resume so the visible `Steer:` entry is sent without
|
|
269
|
+
// waiting for the current tool/model boundary.
|
|
270
|
+
if (!text && this.ctx.session.isStreaming) {
|
|
271
|
+
const queuedMessages = this.ctx.session.getQueuedMessages();
|
|
272
|
+
if (queuedMessages.steering.length > 0) {
|
|
273
|
+
await this.ctx.session.interruptAndFlushQueuedMessages({ reason: USER_INTERRUPT_LABEL });
|
|
274
|
+
this.ctx.updatePendingMessagesDisplay();
|
|
275
|
+
this.ctx.ui.requestRender();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (this.ctx.session.queuedMessageCount > 0) {
|
|
279
|
+
// Preserve the existing empty-submit flush for non-steer queues.
|
|
280
|
+
await this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
248
283
|
}
|
|
249
284
|
|
|
250
285
|
if (!text) return;
|
|
@@ -291,7 +326,6 @@ export class InputController {
|
|
|
291
326
|
// Handle built-in slash commands
|
|
292
327
|
const slashResult = await executeBuiltinSlashCommand(text, {
|
|
293
328
|
ctx: this.ctx,
|
|
294
|
-
handleBackgroundCommand: () => this.handleBackgroundCommand(),
|
|
295
329
|
});
|
|
296
330
|
if (slashResult === true) {
|
|
297
331
|
return;
|
|
@@ -600,7 +634,7 @@ export class InputController {
|
|
|
600
634
|
if (allQueued.length === 0) {
|
|
601
635
|
this.ctx.updatePendingMessagesDisplay();
|
|
602
636
|
if (options?.abort) {
|
|
603
|
-
this.ctx.session.abort();
|
|
637
|
+
this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
604
638
|
}
|
|
605
639
|
return 0;
|
|
606
640
|
}
|
|
@@ -610,128 +644,99 @@ export class InputController {
|
|
|
610
644
|
this.ctx.editor.setText(combinedText);
|
|
611
645
|
this.ctx.updatePendingMessagesDisplay();
|
|
612
646
|
if (options?.abort) {
|
|
613
|
-
this.ctx.session.abort();
|
|
647
|
+
this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
614
648
|
}
|
|
615
649
|
return allQueued.length;
|
|
616
650
|
}
|
|
617
651
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
const backgroundUiContext = this.ctx.createBackgroundUiContext();
|
|
636
|
-
|
|
637
|
-
// Background mode disables interactive UI so tools like ask fail fast.
|
|
638
|
-
this.ctx.setToolUIContext(backgroundUiContext, false);
|
|
639
|
-
this.ctx.initializeHookRunner(backgroundUiContext, false);
|
|
640
|
-
|
|
641
|
-
if (this.ctx.loadingAnimation) {
|
|
642
|
-
this.ctx.loadingAnimation.stop();
|
|
643
|
-
this.ctx.loadingAnimation = undefined;
|
|
644
|
-
}
|
|
645
|
-
if (this.ctx.autoCompactionLoader) {
|
|
646
|
-
this.ctx.autoCompactionLoader.stop();
|
|
647
|
-
this.ctx.autoCompactionLoader = undefined;
|
|
648
|
-
}
|
|
649
|
-
if (this.ctx.retryLoader) {
|
|
650
|
-
this.ctx.retryLoader.stop();
|
|
651
|
-
this.ctx.retryLoader = undefined;
|
|
652
|
-
}
|
|
653
|
-
this.ctx.statusContainer.clear();
|
|
654
|
-
this.ctx.statusLine.dispose();
|
|
655
|
-
|
|
656
|
-
if (this.ctx.unsubscribe) {
|
|
657
|
-
this.ctx.unsubscribe();
|
|
658
|
-
}
|
|
659
|
-
this.ctx.unsubscribe = this.ctx.session.subscribe(async (event: AgentSessionEvent) => {
|
|
660
|
-
await this.ctx.handleBackgroundEvent(event);
|
|
652
|
+
async #insertPendingImage(imageData: ImageContent): Promise<void> {
|
|
653
|
+
const imageLink = (
|
|
654
|
+
await materializeImageReferenceLinks(
|
|
655
|
+
[
|
|
656
|
+
{
|
|
657
|
+
type: "image",
|
|
658
|
+
data: imageData.data,
|
|
659
|
+
mimeType: imageData.mimeType,
|
|
660
|
+
},
|
|
661
|
+
],
|
|
662
|
+
this.ctx.sessionManager.putBlob.bind(this.ctx.sessionManager),
|
|
663
|
+
)
|
|
664
|
+
)?.[0];
|
|
665
|
+
this.ctx.pendingImages.push({
|
|
666
|
+
type: "image",
|
|
667
|
+
data: imageData.data,
|
|
668
|
+
mimeType: imageData.mimeType,
|
|
661
669
|
});
|
|
670
|
+
this.ctx.pendingImageLinks.push(imageLink);
|
|
671
|
+
this.ctx.editor.imageLinks = this.ctx.pendingImageLinks;
|
|
672
|
+
const imageNum = this.ctx.pendingImages.length;
|
|
673
|
+
this.ctx.editor.insertText(`[Image #${imageNum}] `);
|
|
674
|
+
this.ctx.ui.requestRender(false, { allowUnknownViewportMutation: true });
|
|
675
|
+
}
|
|
662
676
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
this.ctx.
|
|
677
|
+
async #normalizeAndInsertPastedImage(image: ImageContent, unsupportedMessage: string): Promise<boolean> {
|
|
678
|
+
let imageData = await ensureSupportedImageInput(image);
|
|
679
|
+
if (!imageData) {
|
|
680
|
+
this.ctx.showStatus(unsupportedMessage);
|
|
681
|
+
return false;
|
|
667
682
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
683
|
+
if (settings.get("images.autoResize")) {
|
|
684
|
+
try {
|
|
685
|
+
const resized = await resizeImage({
|
|
686
|
+
type: "image",
|
|
687
|
+
data: imageData.data,
|
|
688
|
+
mimeType: imageData.mimeType,
|
|
689
|
+
});
|
|
690
|
+
imageData = { type: "image", data: resized.data, mimeType: resized.mimeType };
|
|
691
|
+
} catch {
|
|
692
|
+
// Keep the normalized image when resize fails.
|
|
693
|
+
}
|
|
674
694
|
}
|
|
695
|
+
await this.#insertPendingImage(imageData);
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
675
698
|
|
|
676
|
-
|
|
699
|
+
async handleImagePathPaste(path: string): Promise<void> {
|
|
700
|
+
try {
|
|
701
|
+
const image = await loadImageInput({
|
|
702
|
+
path,
|
|
703
|
+
cwd: this.ctx.sessionManager.getCwd(),
|
|
704
|
+
autoResize: false,
|
|
705
|
+
});
|
|
706
|
+
if (!image) {
|
|
707
|
+
this.ctx.editor.pasteText(path);
|
|
708
|
+
this.ctx.ui.requestRender(false, { allowUnknownViewportMutation: true });
|
|
709
|
+
this.ctx.showStatus("Pasted path is not a supported image");
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
await this.#normalizeAndInsertPastedImage(
|
|
713
|
+
{ type: "image", data: image.data, mimeType: image.mimeType },
|
|
714
|
+
`Unsupported pasted image format: ${image.mimeType}`,
|
|
715
|
+
);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
this.ctx.editor.pasteText(path);
|
|
718
|
+
this.ctx.ui.requestRender(false, { allowUnknownViewportMutation: true });
|
|
719
|
+
this.ctx.showStatus(
|
|
720
|
+
error instanceof ImageInputTooLargeError ? error.message : "Failed to read pasted image path",
|
|
721
|
+
);
|
|
722
|
+
}
|
|
677
723
|
}
|
|
678
724
|
|
|
679
725
|
async handleImagePaste(): Promise<boolean> {
|
|
680
726
|
try {
|
|
681
727
|
const image = await readImageFromClipboard();
|
|
682
|
-
if (image) {
|
|
683
|
-
|
|
684
|
-
|
|
728
|
+
if (!image) {
|
|
729
|
+
this.ctx.showStatus("No image in clipboard (use terminal paste for text)");
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
return await this.#normalizeAndInsertPastedImage(
|
|
733
|
+
{
|
|
685
734
|
type: "image",
|
|
686
|
-
data:
|
|
735
|
+
data: image.data.toBase64(),
|
|
687
736
|
mimeType: image.mimeType,
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
return false;
|
|
692
|
-
}
|
|
693
|
-
if (settings.get("images.autoResize")) {
|
|
694
|
-
try {
|
|
695
|
-
const resized = await resizeImage({
|
|
696
|
-
type: "image",
|
|
697
|
-
data: imageData.data,
|
|
698
|
-
mimeType: imageData.mimeType,
|
|
699
|
-
});
|
|
700
|
-
imageData = { type: "image", data: resized.data, mimeType: resized.mimeType };
|
|
701
|
-
} catch {
|
|
702
|
-
// Keep the normalized image when resize fails.
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
const imageLink = (
|
|
707
|
-
await materializeImageReferenceLinks(
|
|
708
|
-
[
|
|
709
|
-
{
|
|
710
|
-
type: "image",
|
|
711
|
-
data: imageData.data,
|
|
712
|
-
mimeType: imageData.mimeType,
|
|
713
|
-
},
|
|
714
|
-
],
|
|
715
|
-
this.ctx.sessionManager.putBlob.bind(this.ctx.sessionManager),
|
|
716
|
-
)
|
|
717
|
-
)?.[0];
|
|
718
|
-
this.ctx.pendingImages.push({
|
|
719
|
-
type: "image",
|
|
720
|
-
data: imageData.data,
|
|
721
|
-
mimeType: imageData.mimeType,
|
|
722
|
-
});
|
|
723
|
-
this.ctx.pendingImageLinks.push(imageLink);
|
|
724
|
-
this.ctx.editor.imageLinks = this.ctx.pendingImageLinks;
|
|
725
|
-
// Insert placeholder at cursor like Claude does
|
|
726
|
-
const imageNum = this.ctx.pendingImages.length;
|
|
727
|
-
const placeholder = `[Image #${imageNum}]`;
|
|
728
|
-
this.ctx.editor.insertText(`${placeholder} `);
|
|
729
|
-
this.ctx.ui.requestRender();
|
|
730
|
-
return true;
|
|
731
|
-
}
|
|
732
|
-
// No image in clipboard - show hint
|
|
733
|
-
this.ctx.showStatus("No image in clipboard (use terminal paste for text)");
|
|
734
|
-
return false;
|
|
737
|
+
},
|
|
738
|
+
`Unsupported clipboard image format: ${image.mimeType}`,
|
|
739
|
+
);
|
|
735
740
|
} catch {
|
|
736
741
|
this.ctx.showStatus("Failed to read clipboard");
|
|
737
742
|
return false;
|