@oh-my-pi/pi-coding-agent 16.1.23 → 16.2.0
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 +70 -0
- package/dist/cli.js +3817 -3735
- package/dist/types/advisor/__tests__/emission-guard.test.d.ts +1 -0
- package/dist/types/advisor/emission-guard.d.ts +73 -0
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/runtime.d.ts +7 -0
- package/dist/types/advisor/watchdog.d.ts +2 -0
- package/dist/types/cli/flag-tables.d.ts +1 -0
- package/dist/types/cli/gallery-fixtures/search.d.ts +1 -1
- package/dist/types/collab/display-name.d.ts +3 -0
- package/dist/types/collab/host.d.ts +0 -2
- package/dist/types/commands/worktree.d.ts +0 -3
- package/dist/types/config/inline-tool-descriptors-mode.d.ts +13 -0
- package/dist/types/config/model-discovery.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +6 -2
- package/dist/types/config/model-resolver.d.ts +12 -0
- package/dist/types/config/models-config-schema.d.ts +29 -2
- package/dist/types/config/models-config.d.ts +22 -1
- package/dist/types/config/settings-schema.d.ts +91 -21
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/dap/config.d.ts +1 -1
- package/dist/types/edit/hashline/filesystem.d.ts +4 -2
- package/dist/types/edit/renderer.d.ts +4 -0
- package/dist/types/extensibility/extensions/types.d.ts +17 -17
- package/dist/types/extensibility/hooks/types.d.ts +10 -10
- package/dist/types/extensibility/plugins/marketplace/types.d.ts +1 -0
- package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +1 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/registry-helpers.d.ts +2 -0
- package/dist/types/internal-urls/router.d.ts +1 -1
- package/dist/types/internal-urls/ssh-protocol.d.ts +10 -0
- package/dist/types/internal-urls/types.d.ts +19 -2
- package/dist/types/irc/bus.d.ts +6 -0
- package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +7 -0
- package/dist/types/modes/components/move-overlay.d.ts +23 -0
- package/dist/types/modes/components/plugin-selector.d.ts +2 -1
- package/dist/types/modes/components/queue-mode-selector.d.ts +2 -1
- package/dist/types/modes/components/select-list-mouse-routing.d.ts +2 -0
- package/dist/types/modes/components/settings-defs.d.ts +4 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/show-images-selector.d.ts +2 -1
- package/dist/types/modes/components/status-line/component.d.ts +2 -0
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/theme-selector.d.ts +2 -1
- package/dist/types/modes/components/thinking-selector.d.ts +2 -1
- package/dist/types/modes/controllers/command-controller.d.ts +10 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -1
- package/dist/types/modes/internal-url-autocomplete.d.ts +1 -1
- package/dist/types/modes/running-subagent-badge.d.ts +6 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +9 -1
- package/dist/types/sdk.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +2 -1
- package/dist/types/session/session-manager.d.ts +13 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
- package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +1 -0
- package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +1 -0
- package/dist/types/ssh/connection-manager.d.ts +2 -0
- package/dist/types/ssh/file-transfer.d.ts +79 -0
- package/dist/types/ssh/utils.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +3 -0
- package/dist/types/task/executor.d.ts +16 -0
- package/dist/types/tiny/text.d.ts +1 -1
- package/dist/types/tools/builtin-names.d.ts +5 -1
- package/dist/types/tools/{find.d.ts → glob.d.ts} +15 -15
- package/dist/types/tools/{search.d.ts → grep.d.ts} +14 -14
- package/dist/types/tools/index.d.ts +3 -3
- package/dist/types/tools/path-utils.d.ts +29 -0
- package/dist/types/tools/read.d.ts +2 -2
- package/dist/types/tools/render-utils.d.ts +8 -0
- package/dist/types/tools/renderers.d.ts +11 -0
- package/dist/types/tools/ssh.d.ts +2 -0
- package/dist/types/tools/todo.d.ts +0 -16
- package/dist/types/tools/write.d.ts +2 -2
- package/dist/types/utils/active-repo-context.d.ts +8 -0
- package/dist/types/utils/image-resize.d.ts +1 -0
- package/dist/types/utils/markit-cache.d.ts +23 -0
- package/dist/types/utils/markit.d.ts +5 -1
- package/dist/types/utils/prompt-path.d.ts +1 -0
- package/dist/types/web/search/providers/duckduckgo.d.ts +14 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +28 -0
- package/dist/types/web/search/providers/tinyfish.d.ts +29 -0
- package/dist/types/web/search/providers/xai.d.ts +13 -0
- package/dist/types/web/search/types.d.ts +18 -2
- package/package.json +30 -15
- package/scripts/build-binary.ts +9 -9
- package/scripts/bundle-dist.ts +2 -2
- package/src/advisor/__tests__/advisor.test.ts +40 -4
- package/src/advisor/__tests__/emission-guard.test.ts +147 -0
- package/src/advisor/advise-tool.ts +1 -1
- package/src/advisor/emission-guard.ts +172 -0
- package/src/advisor/index.ts +1 -0
- package/src/advisor/runtime.ts +11 -0
- package/src/advisor/watchdog.ts +12 -1
- package/src/cli/args.ts +5 -2
- package/src/cli/auth-broker-cli.ts +17 -0
- package/src/cli/config-cli.ts +4 -0
- package/src/cli/flag-tables.ts +7 -4
- package/src/cli/gallery-cli.ts +14 -2
- package/src/cli/gallery-fixtures/edit.ts +60 -0
- package/src/cli/gallery-fixtures/fs.ts +17 -17
- package/src/cli/gallery-fixtures/search.ts +4 -4
- package/src/cli/usage-cli.ts +20 -1
- package/src/cli/web-search-cli.ts +1 -1
- package/src/collab/display-name.ts +13 -0
- package/src/collab/guest.ts +5 -1
- package/src/collab/host.ts +2 -13
- package/src/commands/worktree.ts +6 -0
- package/src/config/inline-tool-descriptors-mode.ts +27 -0
- package/src/config/model-discovery.ts +77 -8
- package/src/config/model-registry.ts +89 -11
- package/src/config/model-resolver.ts +39 -1
- package/src/config/models-config-schema.ts +41 -5
- package/src/config/models-config.ts +4 -1
- package/src/config/settings-schema.ts +85 -27
- package/src/config/settings.ts +172 -1
- package/src/cursor.ts +1 -1
- package/src/dap/config.ts +152 -8
- package/src/dap/session.ts +1 -1
- package/src/discovery/helpers.ts +3 -1
- package/src/edit/hashline/diff.ts +14 -3
- package/src/edit/hashline/execute.ts +42 -6
- package/src/edit/hashline/filesystem.ts +38 -2
- package/src/edit/index.ts +1 -0
- package/src/edit/modes/patch.ts +11 -1
- package/src/edit/renderer.ts +140 -13
- package/src/eval/__tests__/agent-bridge.test.ts +101 -0
- package/src/eval/agent-bridge.ts +26 -3
- package/src/eval/js/tool-bridge.ts +2 -2
- package/src/export/html/index.ts +1 -1
- package/src/export/html/template.js +3 -1
- package/src/export/html/tool-views.generated.js +20 -20
- package/src/extensibility/extensions/types.ts +22 -22
- package/src/extensibility/hooks/types.ts +11 -11
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +8 -3
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +25 -10
- package/src/extensibility/plugins/marketplace/manager.ts +22 -0
- package/src/extensibility/plugins/marketplace/types.ts +1 -0
- package/src/internal-urls/__tests__/ssh-protocol.test.ts +331 -0
- package/src/internal-urls/docs-index.generated.txt +2 -2
- package/src/internal-urls/docs-index.ts +2 -3
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/registry-helpers.ts +19 -4
- package/src/internal-urls/router.ts +5 -3
- package/src/internal-urls/ssh-protocol.ts +367 -0
- package/src/internal-urls/types.ts +19 -2
- package/src/irc/bus.ts +11 -3
- package/src/mcp/tool-bridge.ts +32 -2
- package/src/memories/index.ts +1 -1
- package/src/modes/acp/acp-event-mapper.ts +2 -2
- package/src/modes/components/__tests__/move-overlay.test.ts +166 -0
- package/src/modes/components/agent-hub.ts +3 -0
- package/src/modes/components/agent-transcript-viewer.ts +9 -7
- package/src/modes/components/custom-editor.ts +90 -5
- package/src/modes/components/move-overlay.ts +282 -0
- package/src/modes/components/plan-review-overlay.ts +35 -35
- package/src/modes/components/plugin-selector.ts +6 -1
- package/src/modes/components/queue-mode-selector.ts +6 -1
- package/src/modes/components/select-list-mouse-routing.ts +35 -0
- package/src/modes/components/session-selector.ts +11 -10
- package/src/modes/components/settings-defs.ts +14 -1
- package/src/modes/components/settings-selector.ts +196 -29
- package/src/modes/components/show-images-selector.ts +6 -1
- package/src/modes/components/status-line/component.ts +108 -28
- package/src/modes/components/status-line/segments.ts +5 -1
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/theme-selector.ts +6 -1
- package/src/modes/components/thinking-selector.ts +6 -1
- package/src/modes/components/tool-execution.ts +25 -9
- package/src/modes/components/tree-selector.ts +5 -5
- package/src/modes/controllers/command-controller.ts +140 -47
- package/src/modes/controllers/event-controller.ts +59 -3
- package/src/modes/controllers/input-controller.ts +68 -2
- package/src/modes/controllers/selector-controller.ts +5 -2
- package/src/modes/controllers/streaming-reveal.ts +17 -0
- package/src/modes/controllers/tool-args-reveal.ts +1 -1
- package/src/modes/interactive-mode.ts +116 -50
- package/src/modes/internal-url-autocomplete.ts +17 -2
- package/src/modes/prompt-action-autocomplete.ts +3 -1
- package/src/modes/running-subagent-badge.ts +13 -0
- package/src/modes/setup-wizard/scenes/glyph.ts +2 -13
- package/src/modes/setup-wizard/scenes/providers.ts +2 -1
- package/src/modes/setup-wizard/scenes/theme.ts +6 -12
- package/src/modes/setup-wizard/scenes/web-search.ts +8 -13
- package/src/modes/setup-wizard/wizard-overlay.ts +6 -5
- package/src/modes/theme/theme.ts +9 -1
- package/src/modes/types.ts +9 -1
- package/src/modes/utils/interactive-context-helpers.ts +1 -1
- package/src/priority.json +15 -0
- package/src/prompts/advisor/active-repo-watchdog.md +6 -0
- package/src/prompts/advisor/system.md +21 -7
- package/src/prompts/agents/designer.md +1 -1
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/agents/librarian.md +2 -2
- package/src/prompts/agents/plan.md +2 -2
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/task.md +2 -2
- package/src/prompts/system/active-repo-context.md +4 -0
- package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
- package/src/prompts/system/plan-mode-active.md +3 -3
- package/src/prompts/system/project-prompt.md +2 -2
- package/src/prompts/system/system-prompt.md +6 -6
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/{find.md → glob.md} +1 -1
- package/src/prompts/tools/{search.md → grep.md} +3 -3
- package/src/prompts/tools/read.md +4 -2
- package/src/sdk.ts +55 -25
- package/src/session/agent-session.ts +508 -132
- package/src/session/session-history-format.ts +2 -2
- package/src/session/session-manager.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +138 -20
- package/src/slash-commands/helpers/usage-report.ts +23 -2
- package/src/ssh/__tests__/connection-manager-args.test.ts +69 -0
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +68 -0
- package/src/ssh/connection-manager.ts +12 -15
- package/src/ssh/file-transfer.ts +209 -0
- package/src/ssh/utils.ts +24 -0
- package/src/system-prompt.ts +56 -28
- package/src/task/executor.ts +96 -36
- package/src/task/index.ts +3 -3
- package/src/task/render.ts +14 -13
- package/src/task/worktree.ts +38 -1
- package/src/tiny/text.ts +49 -4
- package/src/tiny/worker.ts +3 -3
- package/src/tools/bash.ts +2 -2
- package/src/tools/builtin-names.ts +26 -2
- package/src/tools/{find.ts → glob.ts} +48 -42
- package/src/tools/{search.ts → grep.ts} +311 -129
- package/src/tools/index.ts +13 -14
- package/src/tools/irc.ts +6 -2
- package/src/tools/path-utils.ts +64 -1
- package/src/tools/read.ts +35 -6
- package/src/tools/render-utils.ts +14 -0
- package/src/tools/renderers.ts +15 -4
- package/src/tools/ssh.ts +17 -2
- package/src/tools/todo.ts +4 -26
- package/src/tools/write.ts +25 -9
- package/src/utils/active-repo-context.ts +143 -0
- package/src/utils/image-resize.ts +88 -7
- package/src/utils/lang-from-path.ts +3 -3
- package/src/utils/markit-cache.ts +166 -0
- package/src/utils/markit.ts +86 -18
- package/src/utils/prompt-path.ts +3 -0
- package/src/utils/title-generator.ts +1 -1
- package/src/web/search/provider.ts +54 -34
- package/src/web/search/providers/duckduckgo.ts +140 -0
- package/src/web/search/providers/firecrawl.ts +144 -0
- package/src/web/search/providers/tinyfish.ts +159 -0
- package/src/web/search/providers/xai.ts +292 -0
- package/src/web/search/providers/zai.ts +142 -56
- package/src/web/search/types.ts +6 -2
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getEnvApiKey,
|
|
7
7
|
getProviderDetails,
|
|
8
8
|
type ProviderDetails,
|
|
9
|
+
resolveUsedFraction,
|
|
9
10
|
type UsageLimit,
|
|
10
11
|
type UsageReport,
|
|
11
12
|
} from "@oh-my-pi/pi-ai";
|
|
@@ -30,6 +31,7 @@ import { BashExecutionComponent } from "../../modes/components/bash-execution";
|
|
|
30
31
|
import { BorderedLoader } from "../../modes/components/bordered-loader";
|
|
31
32
|
import { DynamicBorder } from "../../modes/components/dynamic-border";
|
|
32
33
|
import { EvalExecutionComponent } from "../../modes/components/eval-execution";
|
|
34
|
+
import { MoveOverlay, type MoveOverlayResult } from "../../modes/components/move-overlay";
|
|
33
35
|
import { TranscriptBlock } from "../../modes/components/transcript-container";
|
|
34
36
|
import { getMarkdownTheme, getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
35
37
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
@@ -40,6 +42,7 @@ import type { AsyncJobSnapshotItem } from "../../session/agent-session";
|
|
|
40
42
|
import type { AuthStorage, OAuthAccountIdentity } from "../../session/auth-storage";
|
|
41
43
|
import type { CompactMode } from "../../session/compact-modes";
|
|
42
44
|
import type { NewSessionOptions } from "../../session/session-entries";
|
|
45
|
+
import { SessionManager } from "../../session/session-manager";
|
|
43
46
|
import { formatShakeSummary, type ShakeMode, type ShakeResult } from "../../session/shake-types";
|
|
44
47
|
import { limitMatchesActiveAccount } from "../../slash-commands/helpers/active-oauth-account";
|
|
45
48
|
import { outputMeta } from "../../tools/output-meta";
|
|
@@ -910,13 +913,34 @@ export class CommandController {
|
|
|
910
913
|
]);
|
|
911
914
|
}
|
|
912
915
|
|
|
913
|
-
|
|
916
|
+
/**
|
|
917
|
+
* `/move` — switch to a fresh empty session in a different directory.
|
|
918
|
+
*
|
|
919
|
+
* With no `targetPath` (TUI only), opens an autocomplete overlay so the user
|
|
920
|
+
* can pick or type a directory. With a `targetPath`, resolves it directly.
|
|
921
|
+
* If the target directory does not exist, the user is asked whether to create
|
|
922
|
+
* it. A brand-new empty session is then started in the target directory and
|
|
923
|
+
* the current session is left behind (resumable via `/resume`).
|
|
924
|
+
*/
|
|
925
|
+
async handleMoveCommand(targetPath?: string): Promise<void> {
|
|
914
926
|
if (this.ctx.session.isStreaming) {
|
|
915
927
|
this.ctx.showWarning("Wait for the current response to finish or abort it before moving.");
|
|
916
928
|
return;
|
|
917
929
|
}
|
|
918
930
|
|
|
919
|
-
|
|
931
|
+
let input: string | undefined = targetPath?.trim() || undefined;
|
|
932
|
+
|
|
933
|
+
// No argument in TUI mode: open the path autocomplete overlay.
|
|
934
|
+
if (!input) {
|
|
935
|
+
const result = await this.ctx.showHookCustom<MoveOverlayResult | undefined>(
|
|
936
|
+
(_tui, _theme, _keybindings, done) => new MoveOverlay(this.ctx.sessionManager.getCwd(), done),
|
|
937
|
+
{ overlay: true },
|
|
938
|
+
);
|
|
939
|
+
if (!result) return; // cancelled
|
|
940
|
+
input = result.directory;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const unquoted = stripOuterDoubleQuotes(input);
|
|
920
944
|
if (!unquoted) {
|
|
921
945
|
this.ctx.showError("Usage: /move <path>");
|
|
922
946
|
return;
|
|
@@ -925,29 +949,85 @@ export class CommandController {
|
|
|
925
949
|
const cwd = this.ctx.sessionManager.getCwd();
|
|
926
950
|
const resolvedPath = resolveToCwd(unquoted, cwd);
|
|
927
951
|
|
|
952
|
+
// If the directory doesn't exist, offer to create it.
|
|
953
|
+
let isDirectory: boolean;
|
|
928
954
|
try {
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
955
|
+
isDirectory = (await fs.stat(resolvedPath)).isDirectory();
|
|
956
|
+
} catch {
|
|
957
|
+
isDirectory = false;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
if (!isDirectory) {
|
|
961
|
+
const parentDir = path.dirname(resolvedPath);
|
|
962
|
+
let parentExists = false;
|
|
963
|
+
try {
|
|
964
|
+
parentExists = (await fs.stat(parentDir)).isDirectory();
|
|
965
|
+
} catch {
|
|
966
|
+
parentExists = false;
|
|
967
|
+
}
|
|
968
|
+
if (!parentExists) {
|
|
969
|
+
this.ctx.showError(`Cannot create "${path.basename(resolvedPath)}": parent directory does not exist`);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
const confirmed = await this.ctx.showHookConfirm(
|
|
973
|
+
"Create directory?",
|
|
974
|
+
`"${path.basename(resolvedPath)}" does not exist. Create it?`,
|
|
975
|
+
);
|
|
976
|
+
if (!confirmed) return;
|
|
977
|
+
try {
|
|
978
|
+
await fs.mkdir(resolvedPath, { recursive: true });
|
|
979
|
+
} catch (err) {
|
|
980
|
+
this.ctx.showError(`Failed to create directory: ${err instanceof Error ? err.message : String(err)}`);
|
|
932
981
|
return;
|
|
933
982
|
}
|
|
934
|
-
} catch {
|
|
935
|
-
this.ctx.showError(`Directory does not exist: ${resolvedPath}`);
|
|
936
|
-
return;
|
|
937
983
|
}
|
|
938
984
|
|
|
985
|
+
let newSessionFile: string | undefined;
|
|
939
986
|
try {
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
this.ctx.
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
987
|
+
// Create a fresh empty session file in the target directory's session
|
|
988
|
+
// folder, then switch to it. The current session is left behind and
|
|
989
|
+
// remains resumable via /resume.
|
|
990
|
+
newSessionFile = SessionManager.createEmptySessionFile(resolvedPath);
|
|
991
|
+
const switched = await this.ctx.session.switchSession(newSessionFile);
|
|
992
|
+
if (!switched) {
|
|
993
|
+
await this.ctx.sessionManager.dropSession(newSessionFile);
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
948
996
|
} catch (err) {
|
|
997
|
+
if (newSessionFile) {
|
|
998
|
+
try {
|
|
999
|
+
await this.ctx.sessionManager.dropSession(newSessionFile);
|
|
1000
|
+
} catch (dropErr) {
|
|
1001
|
+
this.ctx.showError(
|
|
1002
|
+
`Move failed: ${err instanceof Error ? err.message : String(err)}; failed to remove empty session: ${dropErr instanceof Error ? dropErr.message : String(dropErr)}`,
|
|
1003
|
+
);
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
949
1007
|
this.ctx.showError(`Move failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1008
|
+
return;
|
|
950
1009
|
}
|
|
1010
|
+
|
|
1011
|
+
this.ctx.session.markMovedFromEmptySessionFile(newSessionFile!);
|
|
1012
|
+
await this.ctx.applyCwdChange(resolvedPath);
|
|
1013
|
+
|
|
1014
|
+
this.ctx.chatContainer.clear();
|
|
1015
|
+
this.ctx.pendingMessagesContainer.clear();
|
|
1016
|
+
this.ctx.compactionQueuedMessages = [];
|
|
1017
|
+
this.ctx.streamingComponent = undefined;
|
|
1018
|
+
this.ctx.streamingMessage = undefined;
|
|
1019
|
+
this.ctx.pendingTools.clear();
|
|
1020
|
+
this.ctx.statusLine.invalidate();
|
|
1021
|
+
this.ctx.statusLine.setSessionStartTime(Date.now());
|
|
1022
|
+
this.ctx.updateEditorTopBorder();
|
|
1023
|
+
this.ctx.updateEditorBorderColor();
|
|
1024
|
+
await this.ctx.reloadTodos();
|
|
1025
|
+
this.ctx.ui.requestRender(true, { clearScrollback: true });
|
|
1026
|
+
|
|
1027
|
+
this.ctx.present([
|
|
1028
|
+
new Spacer(1),
|
|
1029
|
+
new Text(`${theme.fg("accent", `${theme.status.success} Moved to ${resolvedPath}`)}`, 1, 1),
|
|
1030
|
+
]);
|
|
951
1031
|
}
|
|
952
1032
|
|
|
953
1033
|
async handleRenameCommand(title: string): Promise<void> {
|
|
@@ -1306,22 +1386,10 @@ export function renderProviderSection(details: ProviderDetails, uiTheme: Pick<ty
|
|
|
1306
1386
|
return `${lines.join("\n")}\n`;
|
|
1307
1387
|
}
|
|
1308
1388
|
|
|
1309
|
-
function resolveFraction(limit: UsageLimit): number | undefined {
|
|
1310
|
-
const amount = limit.amount;
|
|
1311
|
-
if (amount.usedFraction !== undefined) return amount.usedFraction;
|
|
1312
|
-
if (amount.used !== undefined && amount.limit !== undefined && amount.limit > 0) {
|
|
1313
|
-
return amount.used / amount.limit;
|
|
1314
|
-
}
|
|
1315
|
-
if (amount.unit === "percent" && amount.used !== undefined) {
|
|
1316
|
-
return amount.used / 100;
|
|
1317
|
-
}
|
|
1318
|
-
return undefined;
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
1389
|
function resolveProviderUsageTotal(reports: UsageReport[]): number {
|
|
1322
1390
|
return reports
|
|
1323
1391
|
.flatMap(report => report.limits)
|
|
1324
|
-
.map(limit =>
|
|
1392
|
+
.map(limit => resolveUsedFraction(limit) ?? 0)
|
|
1325
1393
|
.reduce((sum, value) => sum + value, 0);
|
|
1326
1394
|
}
|
|
1327
1395
|
|
|
@@ -1342,22 +1410,28 @@ function formatWindowSuffix(label: string, windowLabel: string, uiTheme: typeof
|
|
|
1342
1410
|
}
|
|
1343
1411
|
|
|
1344
1412
|
function formatAccountLabel(limit: UsageLimit, report: UsageReport, index: number): string {
|
|
1345
|
-
const email =
|
|
1346
|
-
if (email) return email;
|
|
1347
|
-
const accountId =
|
|
1413
|
+
const email = report.metadata?.email;
|
|
1414
|
+
if (typeof email === "string" && email) return email;
|
|
1415
|
+
const accountId =
|
|
1416
|
+
typeof report.metadata?.accountId === "string" && report.metadata.accountId
|
|
1417
|
+
? report.metadata.accountId
|
|
1418
|
+
: limit.scope.accountId || undefined;
|
|
1348
1419
|
if (accountId) return accountId;
|
|
1349
|
-
const projectId =
|
|
1420
|
+
const projectId =
|
|
1421
|
+
typeof report.metadata?.projectId === "string" && report.metadata.projectId
|
|
1422
|
+
? report.metadata.projectId
|
|
1423
|
+
: limit.scope.projectId || undefined;
|
|
1350
1424
|
if (projectId) return projectId;
|
|
1351
1425
|
return `account ${index + 1}`;
|
|
1352
1426
|
}
|
|
1353
1427
|
|
|
1354
1428
|
function formatUnlimitedReportLabel(report: UsageReport, index: number): string {
|
|
1355
|
-
const email = report.metadata?.email
|
|
1356
|
-
if (email) return email;
|
|
1357
|
-
const accountId = report.metadata?.accountId
|
|
1358
|
-
if (accountId) return accountId;
|
|
1359
|
-
const projectId = report.metadata?.projectId
|
|
1360
|
-
if (projectId) return projectId;
|
|
1429
|
+
const email = report.metadata?.email;
|
|
1430
|
+
if (typeof email === "string" && email) return email;
|
|
1431
|
+
const accountId = report.metadata?.accountId;
|
|
1432
|
+
if (typeof accountId === "string" && accountId) return accountId;
|
|
1433
|
+
const projectId = report.metadata?.projectId;
|
|
1434
|
+
if (typeof projectId === "string" && projectId) return projectId;
|
|
1361
1435
|
return `account ${index + 1}`;
|
|
1362
1436
|
}
|
|
1363
1437
|
|
|
@@ -1432,7 +1506,7 @@ function resolveAggregateStatus(limits: UsageLimit[]): UsageLimit["status"] {
|
|
|
1432
1506
|
|
|
1433
1507
|
function formatAggregateAmount(limits: UsageLimit[]): string {
|
|
1434
1508
|
const fractions = limits
|
|
1435
|
-
.map(limit =>
|
|
1509
|
+
.map(limit => resolveUsedFraction(limit))
|
|
1436
1510
|
.filter((value): value is number => value !== undefined);
|
|
1437
1511
|
if (fractions.length === limits.length && fractions.length > 0) {
|
|
1438
1512
|
const sum = fractions.reduce((total, value) => total + value, 0);
|
|
@@ -1489,7 +1563,7 @@ function resolveStatusColor(status: UsageLimit["status"]): "success" | "warning"
|
|
|
1489
1563
|
}
|
|
1490
1564
|
|
|
1491
1565
|
function renderUsageBar(limit: UsageLimit, uiTheme: typeof theme, barWidth: number): string {
|
|
1492
|
-
const fraction =
|
|
1566
|
+
const fraction = resolveUsedFraction(limit);
|
|
1493
1567
|
if (fraction === undefined) {
|
|
1494
1568
|
return uiTheme.fg("dim", "·".repeat(barWidth));
|
|
1495
1569
|
}
|
|
@@ -1597,9 +1671,11 @@ export function renderUsageReports(
|
|
|
1597
1671
|
const count = report.resetCredits?.availableCount ?? 0;
|
|
1598
1672
|
if (count <= 0) continue;
|
|
1599
1673
|
const label =
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1674
|
+
typeof report.metadata?.email === "string" && report.metadata.email
|
|
1675
|
+
? report.metadata.email
|
|
1676
|
+
: typeof report.metadata?.accountId === "string" && report.metadata.accountId
|
|
1677
|
+
? report.metadata.accountId
|
|
1678
|
+
: "account";
|
|
1603
1679
|
const isActive =
|
|
1604
1680
|
!!activeAccount &&
|
|
1605
1681
|
((!!activeAccount.accountId && activeAccount.accountId === report.metadata?.accountId) ||
|
|
@@ -1607,6 +1683,23 @@ export function renderUsageReports(
|
|
|
1607
1683
|
resetAccountLines.push(
|
|
1608
1684
|
` • ${label}: ${count} saved reset${count === 1 ? "" : "s"}${isActive ? " (active)" : ""}`,
|
|
1609
1685
|
);
|
|
1686
|
+
const credits = report.resetCredits?.credits;
|
|
1687
|
+
if (credits) {
|
|
1688
|
+
for (const credit of credits) {
|
|
1689
|
+
if (credit.expiresAt) {
|
|
1690
|
+
const expiryMs = Date.parse(credit.expiresAt);
|
|
1691
|
+
if (!Number.isNaN(expiryMs)) {
|
|
1692
|
+
const remaining = expiryMs - nowMs;
|
|
1693
|
+
const expiryDate = credit.expiresAt.slice(0, 10);
|
|
1694
|
+
if (remaining > 0) {
|
|
1695
|
+
resetAccountLines.push(` expires in ${formatDuration(remaining)} (${expiryDate})`);
|
|
1696
|
+
} else {
|
|
1697
|
+
resetAccountLines.push(` expired (${expiryDate})`);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1610
1703
|
}
|
|
1611
1704
|
if (resetAccountLines.length > 0) {
|
|
1612
1705
|
lines.push(
|
|
@@ -1619,7 +1712,7 @@ export function renderUsageReports(
|
|
|
1619
1712
|
const entries = group.limits.map((limit, index) => ({
|
|
1620
1713
|
limit,
|
|
1621
1714
|
report: group.reports[index],
|
|
1622
|
-
fraction:
|
|
1715
|
+
fraction: resolveUsedFraction(limit),
|
|
1623
1716
|
index,
|
|
1624
1717
|
}));
|
|
1625
1718
|
entries.sort((a, b) => {
|
|
@@ -1672,8 +1765,8 @@ export function renderUsageReports(
|
|
|
1672
1765
|
const unlimitedReports = providerReports.filter(report => report.limits.length === 0);
|
|
1673
1766
|
for (const report of unlimitedReports) {
|
|
1674
1767
|
const label = formatUnlimitedReportLabel(report, 0);
|
|
1675
|
-
const tier = report.metadata?.planType
|
|
1676
|
-
const tierSuffix = tier ? ` ${uiTheme.fg("dim", `(${tier})`)}` : "";
|
|
1768
|
+
const tier = report.metadata?.planType;
|
|
1769
|
+
const tierSuffix = typeof tier === "string" && tier ? ` ${uiTheme.fg("dim", `(${tier})`)}` : "";
|
|
1677
1770
|
lines.push(
|
|
1678
1771
|
`${uiTheme.fg("success", uiTheme.status.success)} ${label}${tierSuffix} ${uiTheme.fg("dim", "-- no limits")}`,
|
|
1679
1772
|
);
|
|
@@ -5,7 +5,7 @@ import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
|
|
|
5
5
|
import { extractTextContent } from "../../commit/utils";
|
|
6
6
|
import { settings } from "../../config/settings";
|
|
7
7
|
import { getFileSnapshotStore } from "../../edit/file-snapshot-store";
|
|
8
|
-
import
|
|
8
|
+
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
9
9
|
import { detectCacheInvalidation } from "../../modes/components/cache-invalidation-marker";
|
|
10
10
|
import {
|
|
11
11
|
ReadToolGroupComponent,
|
|
@@ -86,12 +86,14 @@ export class EventController {
|
|
|
86
86
|
#lastTtsrNotification: TtsrNotificationComponent | undefined = undefined;
|
|
87
87
|
#streamingReveal: StreamingRevealController;
|
|
88
88
|
#toolArgsReveal: ToolArgsRevealController;
|
|
89
|
+
#prevHideThinking = false;
|
|
89
90
|
#handlers: AgentSessionEventHandlers;
|
|
91
|
+
#terminalProgressActive = false;
|
|
90
92
|
|
|
91
93
|
constructor(private ctx: InteractiveModeContext) {
|
|
92
94
|
this.#streamingReveal = new StreamingRevealController({
|
|
93
95
|
getSmoothStreaming: () => this.ctx.settings.get("display.smoothStreaming"),
|
|
94
|
-
getHideThinkingBlock: () => this.ctx.
|
|
96
|
+
getHideThinkingBlock: () => this.ctx.effectiveHideThinkingBlock,
|
|
95
97
|
getProseOnlyThinking: () => this.ctx.proseOnlyThinking,
|
|
96
98
|
requestRender: () => this.ctx.ui.requestRender(),
|
|
97
99
|
});
|
|
@@ -124,7 +126,27 @@ export class EventController {
|
|
|
124
126
|
thinking_level_changed: async () => {
|
|
125
127
|
this.ctx.statusLine.invalidate();
|
|
126
128
|
this.ctx.updateEditorBorderColor();
|
|
127
|
-
this.ctx.
|
|
129
|
+
const hideThinking = this.ctx.effectiveHideThinkingBlock;
|
|
130
|
+
// Only do the expensive full resetDisplay when the effective
|
|
131
|
+
// visibility actually changed. Auto-classification (e.g. high→medium)
|
|
132
|
+
// emits thinking_level_changed without changing visibility — a full
|
|
133
|
+
// terminal replay for those would be disruptive.
|
|
134
|
+
if (hideThinking === this.#prevHideThinking) {
|
|
135
|
+
this.ctx.ui.requestRender();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
this.#prevHideThinking = hideThinking;
|
|
139
|
+
// Propagate visibility to existing rendered messages.
|
|
140
|
+
for (const child of this.ctx.chatContainer.children) {
|
|
141
|
+
if (child instanceof AssistantMessageComponent) {
|
|
142
|
+
child.setHideThinkingBlock(hideThinking);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (this.ctx.streamingComponent && this.ctx.streamingMessage) {
|
|
146
|
+
this.ctx.streamingComponent.setHideThinkingBlock(hideThinking);
|
|
147
|
+
this.#streamingReveal.resyncVisibility();
|
|
148
|
+
}
|
|
149
|
+
this.ctx.ui.resetDisplay();
|
|
128
150
|
},
|
|
129
151
|
goal_updated: async () => {},
|
|
130
152
|
} satisfies AgentSessionEventHandlers;
|
|
@@ -134,6 +156,7 @@ export class EventController {
|
|
|
134
156
|
this.#streamingReveal.stop();
|
|
135
157
|
this.#toolArgsReveal.stop();
|
|
136
158
|
this.#cancelIdleCompaction();
|
|
159
|
+
this.#setTerminalProgress(false);
|
|
137
160
|
for (const timer of this.#ircExpiryTimers.values()) {
|
|
138
161
|
clearTimeout(timer);
|
|
139
162
|
}
|
|
@@ -248,6 +271,18 @@ export class EventController {
|
|
|
248
271
|
await run(event);
|
|
249
272
|
}
|
|
250
273
|
|
|
274
|
+
#setTerminalProgress(active: boolean): void {
|
|
275
|
+
if (active) {
|
|
276
|
+
if (this.#terminalProgressActive || this.ctx.settings?.get("terminal.showProgress") !== true) return;
|
|
277
|
+
this.ctx.ui.terminal.setProgress(true);
|
|
278
|
+
this.#terminalProgressActive = true;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (!this.#terminalProgressActive) return;
|
|
282
|
+
this.ctx.ui.terminal.setProgress(false);
|
|
283
|
+
this.#terminalProgressActive = false;
|
|
284
|
+
}
|
|
285
|
+
|
|
251
286
|
async #handleAgentStart(_event: Extract<AgentSessionEvent, { type: "agent_start" }>): Promise<void> {
|
|
252
287
|
this.#lastIntent = undefined;
|
|
253
288
|
this.#readToolCallArgs.clear();
|
|
@@ -266,11 +301,13 @@ export class EventController {
|
|
|
266
301
|
this.ctx.statusContainer.clear();
|
|
267
302
|
}
|
|
268
303
|
this.#cancelIdleCompaction();
|
|
304
|
+
this.#setTerminalProgress(true);
|
|
269
305
|
this.ctx.ensureLoadingAnimation();
|
|
270
306
|
this.ctx.ui.requestRender();
|
|
271
307
|
}
|
|
272
308
|
|
|
273
309
|
async #handleMessageStart(event: Extract<AgentSessionEvent, { type: "message_start" }>): Promise<void> {
|
|
310
|
+
this.#ensureWorkingLoaderWhileStreaming();
|
|
274
311
|
if (event.message.role === "hookMessage" || event.message.role === "custom") {
|
|
275
312
|
const signature = `${event.message.role}:${event.message.customType}:${event.message.timestamp}`;
|
|
276
313
|
if (this.#renderedCustomMessages.has(signature)) {
|
|
@@ -513,6 +550,7 @@ export class EventController {
|
|
|
513
550
|
}
|
|
514
551
|
|
|
515
552
|
async #handleMessageUpdate(event: Extract<AgentSessionEvent, { type: "message_update" }>): Promise<void> {
|
|
553
|
+
this.#ensureWorkingLoaderWhileStreaming();
|
|
516
554
|
this.#vocalizeDelta(event);
|
|
517
555
|
if (this.ctx.streamingComponent && event.message.role === "assistant") {
|
|
518
556
|
this.ctx.streamingMessage = event.message;
|
|
@@ -737,6 +775,7 @@ export class EventController {
|
|
|
737
775
|
}
|
|
738
776
|
|
|
739
777
|
async #handleToolExecutionStart(event: Extract<AgentSessionEvent, { type: "tool_execution_start" }>): Promise<void> {
|
|
778
|
+
this.#ensureWorkingLoaderWhileStreaming();
|
|
740
779
|
this.#updateWorkingMessageFromIntent(event.intent);
|
|
741
780
|
this.#resolveDisplaceablePoll(event.toolName);
|
|
742
781
|
if (!this.ctx.pendingTools.has(event.toolCallId)) {
|
|
@@ -801,6 +840,7 @@ export class EventController {
|
|
|
801
840
|
async #handleToolExecutionUpdate(
|
|
802
841
|
event: Extract<AgentSessionEvent, { type: "tool_execution_update" }>,
|
|
803
842
|
): Promise<void> {
|
|
843
|
+
this.#ensureWorkingLoaderWhileStreaming();
|
|
804
844
|
const component = this.ctx.pendingTools.get(event.toolCallId);
|
|
805
845
|
if (component) {
|
|
806
846
|
const asyncState = (event.partialResult.details as { async?: { state?: string } } | undefined)?.async?.state;
|
|
@@ -929,6 +969,7 @@ export class EventController {
|
|
|
929
969
|
}
|
|
930
970
|
|
|
931
971
|
async #finishAgentEnd(): Promise<void> {
|
|
972
|
+
this.#setTerminalProgress(false);
|
|
932
973
|
this.#streamingReveal.stop();
|
|
933
974
|
this.#toolArgsReveal.flushAll();
|
|
934
975
|
if (this.ctx.loadingAnimation) {
|
|
@@ -989,6 +1030,17 @@ export class EventController {
|
|
|
989
1030
|
}
|
|
990
1031
|
}
|
|
991
1032
|
|
|
1033
|
+
/**
|
|
1034
|
+
* Restore the live "Working…" loader when a streaming event lands after a
|
|
1035
|
+
* transient status overlay cleared the container. Focus mode dispatches events
|
|
1036
|
+
* for `viewSession`, so key the reconciler on that session, not the main one.
|
|
1037
|
+
*/
|
|
1038
|
+
#ensureWorkingLoaderWhileStreaming(): void {
|
|
1039
|
+
if (!this.ctx.viewSession.isStreaming) return;
|
|
1040
|
+
if (this.ctx.autoCompactionLoader || this.ctx.retryLoader) return;
|
|
1041
|
+
this.ctx.ensureLoadingAnimation();
|
|
1042
|
+
}
|
|
1043
|
+
|
|
992
1044
|
/**
|
|
993
1045
|
* Trailing Esc hint for live maintenance loaders. While a subagent is
|
|
994
1046
|
* focused, Esc returns to main instead of cancelling its maintenance
|
|
@@ -1004,6 +1056,7 @@ export class EventController {
|
|
|
1004
1056
|
event: Extract<AgentSessionEvent, { type: "auto_compaction_start" }>,
|
|
1005
1057
|
): Promise<void> {
|
|
1006
1058
|
this.#cancelIdleCompaction();
|
|
1059
|
+
this.#setTerminalProgress(true);
|
|
1007
1060
|
this.#stopWorkingLoader();
|
|
1008
1061
|
this.ctx.statusContainer.clear();
|
|
1009
1062
|
const reasonText =
|
|
@@ -1035,6 +1088,7 @@ export class EventController {
|
|
|
1035
1088
|
|
|
1036
1089
|
async #handleAutoCompactionEnd(event: Extract<AgentSessionEvent, { type: "auto_compaction_end" }>): Promise<void> {
|
|
1037
1090
|
this.#cancelIdleCompaction();
|
|
1091
|
+
this.#setTerminalProgress(false);
|
|
1038
1092
|
if (this.ctx.autoCompactionLoader) {
|
|
1039
1093
|
this.ctx.autoCompactionLoader.stop();
|
|
1040
1094
|
this.ctx.autoCompactionLoader = undefined;
|
|
@@ -1096,6 +1150,7 @@ export class EventController {
|
|
|
1096
1150
|
this.ctx.showWarning("Auto context-full maintenance failed; continuing without maintenance");
|
|
1097
1151
|
}
|
|
1098
1152
|
await this.ctx.flushCompactionQueue({ willRetry: event.willRetry });
|
|
1153
|
+
this.#ensureWorkingLoaderWhileStreaming();
|
|
1099
1154
|
this.ctx.ui.requestRender();
|
|
1100
1155
|
}
|
|
1101
1156
|
|
|
@@ -1130,6 +1185,7 @@ export class EventController {
|
|
|
1130
1185
|
if (!event.success) {
|
|
1131
1186
|
this.ctx.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`);
|
|
1132
1187
|
}
|
|
1188
|
+
this.#ensureWorkingLoaderWhileStreaming();
|
|
1133
1189
|
this.ctx.ui.requestRender();
|
|
1134
1190
|
}
|
|
1135
1191
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
3
4
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
4
5
|
import { type AutocompleteProvider, matchesKey, type SlashCommand } from "@oh-my-pi/pi-tui";
|
|
5
6
|
import { $env, isEnoent, logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
@@ -124,6 +125,7 @@ const TINY_TITLE_PROGRESS_REVEAL_DELAY_MS = 1_000;
|
|
|
124
125
|
// deliberate human double-tap is always tens of milliseconds apart.
|
|
125
126
|
const LEFT_DOUBLE_TAP_MIN_GAP_MS = 40;
|
|
126
127
|
const LEFT_DOUBLE_TAP_MAX_GAP_MS = 500;
|
|
128
|
+
const STREAMING_ESCAPE_CANCEL_WINDOW_MS = 2_000;
|
|
127
129
|
|
|
128
130
|
export class InputController {
|
|
129
131
|
constructor(
|
|
@@ -148,6 +150,16 @@ export class InputController {
|
|
|
148
150
|
// (>= LEFT_DOUBLE_TAP_MAX_GAP_MS) starts a fresh sequence. See
|
|
149
151
|
// #detectLeftDoubleTap.
|
|
150
152
|
#leftTapCount = 0;
|
|
153
|
+
// Streaming turns use a two-step Esc: first press arms this token, second press
|
|
154
|
+
// within the window aborts the same live assistant turn. The token is a per-turn
|
|
155
|
+
// sentinel minted lazily on demand and reset on every `agent_start`/`agent_end`
|
|
156
|
+
// (see setupKeyHandlers), so it survives `message_start`/`message_update`
|
|
157
|
+
// transitions inside a single turn but cannot leak across turn boundaries.
|
|
158
|
+
#streamingEscapeTurnSentinel: object | undefined;
|
|
159
|
+
#streamingEscapeArmedToken: object | undefined;
|
|
160
|
+
#streamingEscapeArmedUntil = 0;
|
|
161
|
+
#streamingEscapeTimer: NodeJS.Timeout | undefined;
|
|
162
|
+
#streamingEscapeSessionSubscribed = false;
|
|
151
163
|
// Sequential index for `local://attachment-N` references created by large-paste and
|
|
152
164
|
// pasted-file attachments. Seeded from 0 and bumped past existing attachment files.
|
|
153
165
|
#attachmentCounter = 0;
|
|
@@ -197,8 +209,50 @@ export class InputController {
|
|
|
197
209
|
const unsubscribe = tinyTitleClient.onProgress(update);
|
|
198
210
|
}
|
|
199
211
|
|
|
212
|
+
#clearStreamingEscapeArm(): void {
|
|
213
|
+
this.#streamingEscapeArmedToken = undefined;
|
|
214
|
+
this.#streamingEscapeArmedUntil = 0;
|
|
215
|
+
if (this.#streamingEscapeTimer) {
|
|
216
|
+
clearTimeout(this.#streamingEscapeTimer);
|
|
217
|
+
this.#streamingEscapeTimer = undefined;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#handleStreamingEscape(): void {
|
|
222
|
+
if (!this.#streamingEscapeTurnSentinel) {
|
|
223
|
+
this.#streamingEscapeTurnSentinel = {};
|
|
224
|
+
}
|
|
225
|
+
const token = this.#streamingEscapeTurnSentinel;
|
|
226
|
+
const now = Date.now();
|
|
227
|
+
if (this.#streamingEscapeArmedToken === token && now <= this.#streamingEscapeArmedUntil) {
|
|
228
|
+
this.#clearStreamingEscapeArm();
|
|
229
|
+
void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
this.#clearStreamingEscapeArm();
|
|
234
|
+
this.#streamingEscapeArmedToken = token;
|
|
235
|
+
this.#streamingEscapeArmedUntil = now + STREAMING_ESCAPE_CANCEL_WINDOW_MS;
|
|
236
|
+
this.#streamingEscapeTimer = setTimeout(() => {
|
|
237
|
+
if (this.#streamingEscapeArmedToken === token && Date.now() >= this.#streamingEscapeArmedUntil) {
|
|
238
|
+
this.#clearStreamingEscapeArm();
|
|
239
|
+
}
|
|
240
|
+
}, STREAMING_ESCAPE_CANCEL_WINDOW_MS);
|
|
241
|
+
this.#streamingEscapeTimer.unref?.();
|
|
242
|
+
this.ctx.showStatus("Press Esc again within 2s to cancel streaming.");
|
|
243
|
+
}
|
|
244
|
+
|
|
200
245
|
setupKeyHandlers(): void {
|
|
201
246
|
this.ctx.editor.setActionKeys("app.interrupt", this.ctx.keybindings.getKeys("app.interrupt"));
|
|
247
|
+
if (!this.#streamingEscapeSessionSubscribed && typeof this.ctx.session.subscribe === "function") {
|
|
248
|
+
this.#streamingEscapeSessionSubscribed = true;
|
|
249
|
+
this.ctx.session.subscribe(event => {
|
|
250
|
+
if (event.type === "agent_start" || event.type === "agent_end") {
|
|
251
|
+
this.#streamingEscapeTurnSentinel = undefined;
|
|
252
|
+
this.#clearStreamingEscapeArm();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
202
256
|
if (!this.#focusedLeftTapListenerInstalled) {
|
|
203
257
|
this.#focusedLeftTapListenerInstalled = true;
|
|
204
258
|
this.ctx.ui.addInputListener(data => {
|
|
@@ -274,7 +328,7 @@ export class InputController {
|
|
|
274
328
|
if (this.ctx.loopModeEnabled) {
|
|
275
329
|
this.ctx.pauseLoop();
|
|
276
330
|
if (this.ctx.session.isStreaming) {
|
|
277
|
-
|
|
331
|
+
this.#handleStreamingEscape();
|
|
278
332
|
} else {
|
|
279
333
|
this.ctx.cancelPendingSubmission();
|
|
280
334
|
}
|
|
@@ -325,12 +379,13 @@ export class InputController {
|
|
|
325
379
|
this.ctx.isPythonMode = false;
|
|
326
380
|
this.ctx.updateEditorBorderColor();
|
|
327
381
|
} else if (this.ctx.session.isStreaming) {
|
|
328
|
-
|
|
382
|
+
this.#handleStreamingEscape();
|
|
329
383
|
} else if (this.ctx.editor.getText().trim()) {
|
|
330
384
|
// Esc with typed text clears the draft instead of (or before) any double-Esc action
|
|
331
385
|
this.ctx.editor.setText("");
|
|
332
386
|
this.ctx.ui.requestRender();
|
|
333
387
|
this.ctx.lastEscapeTime = 0;
|
|
388
|
+
this.#clearStreamingEscapeArm();
|
|
334
389
|
} else {
|
|
335
390
|
// Double-interrupt with empty editor triggers /tree, /branch, or nothing based on setting
|
|
336
391
|
const action = settings.get("doubleEscapeAction");
|
|
@@ -1684,6 +1739,17 @@ export class InputController {
|
|
|
1684
1739
|
}
|
|
1685
1740
|
|
|
1686
1741
|
toggleThinkingBlockVisibility(): void {
|
|
1742
|
+
// When thinking is "off", thinking blocks are always hidden (some
|
|
1743
|
+
// providers return them regardless). The toggle is meaningless in
|
|
1744
|
+
// that state — inform the user instead of silently flipping the
|
|
1745
|
+
// persisted value. When thinking is on, the toggle works normally
|
|
1746
|
+
// even if blocks are already hidden (user may want to show them).
|
|
1747
|
+
const thinkingOff =
|
|
1748
|
+
((this.ctx.viewSession ?? this.ctx.session)?.thinkingLevel ?? ThinkingLevel.Off) === ThinkingLevel.Off;
|
|
1749
|
+
if (thinkingOff) {
|
|
1750
|
+
this.ctx.showStatus("Thinking is off — enable thinking to show blocks");
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1687
1753
|
this.ctx.hideThinkingBlock = !this.ctx.hideThinkingBlock;
|
|
1688
1754
|
this.ctx.settings.set("hideThinkingBlock", this.ctx.hideThinkingBlock);
|
|
1689
1755
|
|
|
@@ -134,6 +134,9 @@ export class SelectorController {
|
|
|
134
134
|
availableThinkingLevels: [...this.ctx.session.getAvailableThinkingLevels()],
|
|
135
135
|
thinkingLevel: this.ctx.session.thinkingLevel,
|
|
136
136
|
availableThemes,
|
|
137
|
+
providers: [...new Set(this.ctx.session.getAvailableModels().map(model => model.provider))].sort(
|
|
138
|
+
(a, b) => a.localeCompare(b),
|
|
139
|
+
),
|
|
137
140
|
cwd: getProjectDir(),
|
|
138
141
|
model: this.ctx.session.model,
|
|
139
142
|
imageBudget: this.ctx.ui.imageBudget,
|
|
@@ -341,7 +344,7 @@ export class SelectorController {
|
|
|
341
344
|
this.ctx.hideThinkingBlock = value as boolean;
|
|
342
345
|
for (const child of this.ctx.chatContainer.children) {
|
|
343
346
|
if (child instanceof AssistantMessageComponent) {
|
|
344
|
-
child.setHideThinkingBlock(
|
|
347
|
+
child.setHideThinkingBlock(this.ctx.effectiveHideThinkingBlock);
|
|
345
348
|
}
|
|
346
349
|
}
|
|
347
350
|
// Full clear + replay so blocks frozen in committed scrollback on
|
|
@@ -1302,7 +1305,7 @@ export class SelectorController {
|
|
|
1302
1305
|
getTool: name => this.ctx.session.getToolByName(name),
|
|
1303
1306
|
getMessageRenderer: type => this.ctx.session.extensionRunner?.getMessageRenderer(type),
|
|
1304
1307
|
cwd: this.ctx.sessionManager.getCwd(),
|
|
1305
|
-
hideThinkingBlock: () => this.ctx.
|
|
1308
|
+
hideThinkingBlock: () => this.ctx.effectiveHideThinkingBlock,
|
|
1306
1309
|
proseOnlyThinking: () => this.ctx.proseOnlyThinking,
|
|
1307
1310
|
focusAgent: id => this.ctx.focusAgentSession(id),
|
|
1308
1311
|
sessionFile: this.ctx.sessionManager.getSessionFile() ?? null,
|
|
@@ -270,6 +270,23 @@ export class StreamingRevealController {
|
|
|
270
270
|
this.#unitCounter.reset();
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Re-read cached visibility flags (hideThinkingBlock, proseOnlyThinking)
|
|
275
|
+
* and re-render the current target. Called when the thinking level changes
|
|
276
|
+
* mid-stream so the reveal controller doesn't keep rendering with stale values.
|
|
277
|
+
*/
|
|
278
|
+
resyncVisibility(): void {
|
|
279
|
+
if (!this.#target || !this.#component) return;
|
|
280
|
+
this.#hideThinkingBlock = this.#getHideThinkingBlock();
|
|
281
|
+
this.#proseOnlyThinking = this.#getProseOnlyThinking();
|
|
282
|
+
// Recalculate visible units — hiding thinking blocks may reduce the total,
|
|
283
|
+
// and the reveal position may now exceed it.
|
|
284
|
+
const total = this.#visibleUnits(this.#target);
|
|
285
|
+
this.#revealed = Math.min(this.#revealed, total);
|
|
286
|
+
this.#renderCurrent();
|
|
287
|
+
this.#syncTimer(total);
|
|
288
|
+
}
|
|
289
|
+
|
|
273
290
|
/** Total reveal units of `message`, memoized per block across ticks. */
|
|
274
291
|
#visibleUnits(message: AssistantMessage): number {
|
|
275
292
|
let total = 0;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseStreamingJson } from "@oh-my-pi/pi-
|
|
1
|
+
import { parseStreamingJson } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import { nextStep, STREAMING_REVEAL_FRAME_MS } from "./streaming-reveal";
|
|
3
3
|
|
|
4
4
|
/** Minimal component surface the reveal pushes frames into. */
|