@oh-my-pi/pi-coding-agent 15.7.5 → 15.8.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 +159 -182
- package/dist/types/async/job-manager.d.ts +3 -3
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/claude-trace-cli.d.ts +54 -0
- package/dist/types/cli/session-picker.d.ts +10 -3
- package/dist/types/cli/update-cli.d.ts +17 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +11 -2
- package/dist/types/config/settings.d.ts +13 -0
- package/dist/types/eval/__tests__/heartbeat.test.d.ts +1 -0
- package/dist/types/eval/concurrency-bridge.d.ts +26 -0
- package/dist/types/eval/heartbeat.d.ts +45 -0
- package/dist/types/eval/js/tool-bridge.d.ts +2 -1
- package/dist/types/extensibility/extensions/loader.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +24 -5
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +11 -0
- package/dist/types/lsp/diagnostics-ledger.d.ts +10 -0
- package/dist/types/lsp/index.d.ts +2 -0
- package/dist/types/lsp/utils.d.ts +4 -0
- package/dist/types/main.d.ts +5 -0
- package/dist/types/modes/acp/acp-agent.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/custom-editor.d.ts +3 -2
- package/dist/types/modes/components/hook-selector.d.ts +10 -1
- package/dist/types/modes/components/session-selector.d.ts +32 -5
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -3
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -3
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +5 -3
- package/dist/types/registry/agent-registry.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +12 -3
- package/dist/types/session/history-storage.d.ts +16 -1
- package/dist/types/session/session-manager.d.ts +4 -0
- package/dist/types/task/output-manager.d.ts +6 -15
- package/dist/types/tools/ask.d.ts +8 -6
- package/dist/types/tools/eval-backends.d.ts +12 -0
- package/dist/types/tools/eval-render.d.ts +52 -0
- package/dist/types/tools/eval.d.ts +2 -35
- package/dist/types/tools/find.d.ts +0 -9
- package/dist/types/tools/index.d.ts +5 -12
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/sqlite-reader.d.ts +25 -8
- package/dist/types/tui/output-block.d.ts +4 -3
- package/dist/types/utils/clipboard.d.ts +4 -0
- package/dist/types/web/kagi.d.ts +76 -0
- package/dist/types/web/search/providers/exa.d.ts +7 -1
- package/dist/types/web/search/providers/kagi.d.ts +1 -0
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/thinking-note.ts +13 -0
- package/package.json +9 -9
- package/src/async/job-manager.ts +3 -3
- package/src/cli/args.ts +6 -2
- package/src/cli/claude-trace-cli.ts +783 -0
- package/src/cli/session-picker.ts +36 -10
- package/src/cli/update-cli.ts +35 -2
- package/src/commands/launch.ts +3 -0
- package/src/config/keybindings.ts +6 -0
- package/src/config/model-registry.ts +33 -4
- package/src/config/settings-schema.ts +12 -2
- package/src/config/settings.ts +23 -0
- package/src/discovery/claude-plugins.ts +7 -9
- package/src/discovery/claude.ts +41 -22
- package/src/edit/index.ts +23 -3
- package/src/eval/__tests__/agent-bridge.test.ts +148 -4
- package/src/eval/__tests__/heartbeat.test.ts +84 -0
- package/src/eval/__tests__/llm-bridge.test.ts +30 -0
- package/src/eval/agent-bridge.ts +44 -38
- package/src/eval/concurrency-bridge.ts +34 -0
- package/src/eval/heartbeat.ts +74 -0
- package/src/eval/js/executor.ts +13 -9
- package/src/eval/js/shared/prelude.txt +20 -17
- package/src/eval/js/tool-bridge.ts +5 -0
- package/src/eval/llm-bridge.ts +20 -14
- package/src/eval/py/executor.ts +14 -18
- package/src/eval/py/prelude.py +23 -15
- package/src/exec/bash-executor.ts +31 -5
- package/src/extensibility/extensions/loader.ts +16 -18
- package/src/extensibility/extensions/runner.ts +22 -17
- package/src/extensibility/extensions/types.ts +39 -5
- package/src/extensibility/plugins/legacy-pi-compat.ts +115 -131
- package/src/extensibility/skills.ts +0 -1
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/lsp/diagnostics-ledger.ts +51 -0
- package/src/lsp/index.ts +9 -22
- package/src/lsp/utils.ts +21 -0
- package/src/main.ts +92 -24
- package/src/modes/acp/acp-agent.ts +8 -4
- package/src/modes/acp/acp-event-mapper.ts +54 -4
- package/src/modes/components/assistant-message.ts +28 -1
- package/src/modes/components/custom-editor.ts +19 -7
- package/src/modes/components/hook-selector.ts +229 -44
- package/src/modes/components/oauth-selector.ts +12 -6
- package/src/modes/components/session-selector.ts +179 -24
- package/src/modes/components/tool-execution.ts +36 -7
- package/src/modes/controllers/command-controller.ts +2 -11
- package/src/modes/controllers/event-controller.ts +5 -2
- package/src/modes/controllers/extension-ui-controller.ts +6 -4
- package/src/modes/controllers/input-controller.ts +19 -16
- package/src/modes/controllers/selector-controller.ts +61 -21
- package/src/modes/interactive-mode.ts +127 -16
- package/src/modes/rpc/rpc-mode.ts +17 -6
- package/src/modes/theme/theme-schema.json +30 -0
- package/src/modes/theme/theme.ts +39 -2
- package/src/modes/types.ts +7 -3
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/system/orchestrate-notice.md +5 -3
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/ask.md +2 -1
- package/src/prompts/tools/eval.md +6 -6
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/irc.md +6 -6
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +1 -1
- package/src/registry/agent-registry.ts +1 -1
- package/src/sdk.ts +85 -31
- package/src/session/agent-session.ts +127 -57
- package/src/session/history-storage.ts +56 -12
- package/src/session/session-manager.ts +34 -0
- package/src/task/output-manager.ts +40 -48
- package/src/task/render.ts +3 -8
- package/src/tools/ask.ts +74 -32
- package/src/tools/browser/tab-worker.ts +8 -5
- package/src/tools/eval-backends.ts +38 -0
- package/src/tools/eval-render.ts +750 -0
- package/src/tools/eval.ts +27 -754
- package/src/tools/find.ts +5 -29
- package/src/tools/index.ts +8 -38
- package/src/tools/path-utils.ts +144 -1
- package/src/tools/read.ts +47 -0
- package/src/tools/renderers.ts +1 -1
- package/src/tools/search.ts +2 -27
- package/src/tools/sqlite-reader.ts +92 -9
- package/src/tools/write.ts +9 -1
- package/src/tui/output-block.ts +5 -4
- package/src/utils/clipboard.ts +38 -1
- package/src/utils/open.ts +37 -2
- package/src/web/kagi.ts +168 -49
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/web/search/providers/exa.ts +20 -86
- package/src/web/search/providers/kagi.ts +4 -0
|
@@ -3,7 +3,7 @@ import { getOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
|
|
|
3
3
|
import type { OAuthProvider } from "@oh-my-pi/pi-ai/utils/oauth/types";
|
|
4
4
|
import type { Component, OverlayHandle } from "@oh-my-pi/pi-tui";
|
|
5
5
|
import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
6
|
-
import { getAgentDbPath, getProjectDir } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { getAgentDbPath, getProjectDir, normalizePathForComparison } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { getRoleInfo } from "../../config/model-registry";
|
|
8
8
|
import { formatModelSelectorValue } from "../../config/model-resolver";
|
|
9
9
|
import { settings } from "../../config/settings";
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
setPreferredImageProvider,
|
|
37
37
|
setPreferredSearchProvider,
|
|
38
38
|
} from "../../tools";
|
|
39
|
+
import { shortenPath } from "../../tools/render-utils";
|
|
39
40
|
import { setSessionTerminalTitle } from "../../utils/title-generator";
|
|
40
41
|
import { AgentDashboard } from "../components/agent-dashboard";
|
|
41
42
|
import { AssistantMessageComponent } from "../components/assistant-message";
|
|
@@ -719,12 +720,22 @@ export class SelectorController {
|
|
|
719
720
|
this.ctx.sessionManager.getCwd(),
|
|
720
721
|
this.ctx.sessionManager.getSessionDir(),
|
|
721
722
|
);
|
|
723
|
+
// Current folder has no sessions: preload the global list so the picker
|
|
724
|
+
// can open straight into all-projects scope instead of dead-ending.
|
|
725
|
+
let allSessions: SessionInfo[] | undefined;
|
|
726
|
+
let startInAllScope = false;
|
|
727
|
+
if (sessions.length === 0) {
|
|
728
|
+
allSessions = await SessionManager.listAll();
|
|
729
|
+
startInAllScope = allSessions.length > 0;
|
|
730
|
+
}
|
|
731
|
+
const historyStorage = this.ctx.historyStorage;
|
|
732
|
+
const historyMatcher = historyStorage ? (query: string) => historyStorage.matchingSessionIds(query) : undefined;
|
|
722
733
|
this.showSelector(done => {
|
|
723
734
|
const selector = new SessionSelectorComponent(
|
|
724
735
|
sessions,
|
|
725
|
-
async
|
|
736
|
+
async (session: SessionInfo) => {
|
|
726
737
|
done();
|
|
727
|
-
await this.handleResumeSession(
|
|
738
|
+
await this.handleResumeSession(session.path);
|
|
728
739
|
},
|
|
729
740
|
() => {
|
|
730
741
|
done();
|
|
@@ -733,19 +744,25 @@ export class SelectorController {
|
|
|
733
744
|
() => {
|
|
734
745
|
void this.ctx.shutdown();
|
|
735
746
|
},
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
747
|
+
{
|
|
748
|
+
onDelete: async (session: SessionInfo) => {
|
|
749
|
+
if (!(await this.#detachActiveSessionBeforeDeletion(session.path))) {
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
const storage = new FileSessionStorage();
|
|
753
|
+
try {
|
|
754
|
+
await storage.deleteSessionWithArtifacts(session.path);
|
|
755
|
+
return true;
|
|
756
|
+
} catch (err) {
|
|
757
|
+
throw new Error(`Failed to delete session: ${err instanceof Error ? err.message : String(err)}`, {
|
|
758
|
+
cause: err,
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
},
|
|
762
|
+
historyMatcher,
|
|
763
|
+
loadAllSessions: () => SessionManager.listAll(),
|
|
764
|
+
allSessions,
|
|
765
|
+
startInAllScope,
|
|
749
766
|
},
|
|
750
767
|
);
|
|
751
768
|
selector.setOnRequestRender(() => this.ctx.ui.requestRender());
|
|
@@ -801,8 +818,17 @@ export class SelectorController {
|
|
|
801
818
|
async handleResumeSession(sessionPath: string): Promise<void> {
|
|
802
819
|
this.#clearTransientSessionUi();
|
|
803
820
|
|
|
804
|
-
|
|
821
|
+
const previousCwd = this.ctx.sessionManager.getCwd();
|
|
822
|
+
// Switch session via AgentSession (emits hook and tool session events). The
|
|
823
|
+
// SessionManager adopts the resumed session's own cwd when it differs.
|
|
805
824
|
await this.ctx.session.switchSession(sessionPath);
|
|
825
|
+
const newCwd = this.ctx.sessionManager.getCwd();
|
|
826
|
+
const movedProject = normalizePathForComparison(newCwd) !== normalizePathForComparison(previousCwd);
|
|
827
|
+
if (movedProject) {
|
|
828
|
+
// Resumed a session from another project: re-point the process and every
|
|
829
|
+
// cwd-derived cache at it before rendering.
|
|
830
|
+
await this.ctx.applyCwdChange(newCwd);
|
|
831
|
+
}
|
|
806
832
|
this.#refreshSessionTerminalTitle();
|
|
807
833
|
this.ctx.updateEditorBorderColor();
|
|
808
834
|
|
|
@@ -810,7 +836,7 @@ export class SelectorController {
|
|
|
810
836
|
this.ctx.chatContainer.clear();
|
|
811
837
|
this.ctx.renderInitialMessages(undefined, { clearTerminalHistory: true });
|
|
812
838
|
await this.ctx.reloadTodos();
|
|
813
|
-
this.ctx.showStatus("Resumed session");
|
|
839
|
+
this.ctx.showStatus(movedProject ? `Resumed session in ${shortenPath(newCwd)}` : "Resumed session");
|
|
814
840
|
}
|
|
815
841
|
|
|
816
842
|
async handleSessionDeleteCommand(): Promise<void> {
|
|
@@ -919,7 +945,15 @@ export class SelectorController {
|
|
|
919
945
|
|
|
920
946
|
async #handleOAuthLogout(providerId: string): Promise<void> {
|
|
921
947
|
try {
|
|
922
|
-
|
|
948
|
+
const authStorage = this.ctx.session.modelRegistry.authStorage;
|
|
949
|
+
if (!authStorage.has(providerId)) {
|
|
950
|
+
const source = authStorage.describeCredentialSource(providerId, this.ctx.session.sessionId);
|
|
951
|
+
const suffix = source ? ` Current auth comes from ${source}; remove that source to log out.` : "";
|
|
952
|
+
this.ctx.showError(`Logout skipped: no stored credentials for ${providerId}.${suffix}`);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
await authStorage.logout(providerId);
|
|
923
957
|
await this.ctx.session.modelRegistry.refresh();
|
|
924
958
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
925
959
|
this.ctx.chatContainer.addChild(
|
|
@@ -928,6 +962,12 @@ export class SelectorController {
|
|
|
928
962
|
this.ctx.chatContainer.addChild(
|
|
929
963
|
new Text(theme.fg("dim", `Credentials removed from ${getAgentDbPath()}`), 1, 0),
|
|
930
964
|
);
|
|
965
|
+
const remainingSource = authStorage.describeCredentialSource(providerId, this.ctx.session.sessionId);
|
|
966
|
+
if (remainingSource) {
|
|
967
|
+
this.ctx.chatContainer.addChild(
|
|
968
|
+
new Text(theme.fg("warning", `${providerId} is still authenticated via ${remainingSource}`), 1, 0),
|
|
969
|
+
);
|
|
970
|
+
}
|
|
931
971
|
this.ctx.ui.requestRender();
|
|
932
972
|
} catch (error: unknown) {
|
|
933
973
|
this.ctx.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -948,10 +988,10 @@ export class SelectorController {
|
|
|
948
988
|
await this.#refreshOAuthProviderAuthState();
|
|
949
989
|
const oauthProviders = getOAuthProviders();
|
|
950
990
|
const loggedInProviders = oauthProviders.filter(provider =>
|
|
951
|
-
this.ctx.session.modelRegistry.authStorage.
|
|
991
|
+
this.ctx.session.modelRegistry.authStorage.has(provider.id),
|
|
952
992
|
);
|
|
953
993
|
if (loggedInProviders.length === 0) {
|
|
954
|
-
this.ctx.showStatus("No
|
|
994
|
+
this.ctx.showStatus("No stored provider credentials to log out. Remove env or config auth at its source.");
|
|
955
995
|
return;
|
|
956
996
|
}
|
|
957
997
|
}
|
|
@@ -32,14 +32,29 @@ import {
|
|
|
32
32
|
TUI,
|
|
33
33
|
visibleWidth,
|
|
34
34
|
} from "@oh-my-pi/pi-tui";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
APP_NAME,
|
|
37
|
+
adjustHsv,
|
|
38
|
+
formatNumber,
|
|
39
|
+
getProjectDir,
|
|
40
|
+
hsvToRgb,
|
|
41
|
+
isEnoent,
|
|
42
|
+
logger,
|
|
43
|
+
postmortem,
|
|
44
|
+
prompt,
|
|
45
|
+
setProjectDir,
|
|
46
|
+
} from "@oh-my-pi/pi-utils";
|
|
36
47
|
import chalk from "chalk";
|
|
48
|
+
import { reset as resetCapabilities } from "../capability";
|
|
37
49
|
import { KeybindingsManager } from "../config/keybindings";
|
|
38
50
|
import { MODEL_ROLES, type ModelRole } from "../config/model-registry";
|
|
39
51
|
import { isSettingsInitialized, Settings, settings } from "../config/settings";
|
|
52
|
+
import { clearClaudePluginRootsCache } from "../discovery/helpers";
|
|
40
53
|
import type {
|
|
54
|
+
ContextUsage,
|
|
41
55
|
ExtensionUIContext,
|
|
42
56
|
ExtensionUIDialogOptions,
|
|
57
|
+
ExtensionUISelectItem,
|
|
43
58
|
ExtensionWidgetContent,
|
|
44
59
|
ExtensionWidgetOptions,
|
|
45
60
|
} from "../extensibility/extensions";
|
|
@@ -122,6 +137,7 @@ import type {
|
|
|
122
137
|
CompactionQueuedMessage,
|
|
123
138
|
InteractiveModeContext,
|
|
124
139
|
InteractiveModeInitOptions,
|
|
140
|
+
InteractiveSelectorDialogOptions,
|
|
125
141
|
SubmittedUserInput,
|
|
126
142
|
TodoItem,
|
|
127
143
|
TodoPhase,
|
|
@@ -197,6 +213,8 @@ function formatHudNoteMarker(count: number): string {
|
|
|
197
213
|
type GoalSubcommand = "set" | "show" | "pause" | "resume" | "drop" | "budget";
|
|
198
214
|
|
|
199
215
|
const GOAL_SUBCOMMANDS = new Set<GoalSubcommand>(["set", "show", "pause", "resume", "drop", "budget"]);
|
|
216
|
+
const PLAN_KEEP_CONTEXT_OPTION_INDEX = 2;
|
|
217
|
+
const PLAN_KEEP_CONTEXT_DISABLE_THRESHOLD_PERCENT = 95;
|
|
200
218
|
|
|
201
219
|
function parseGoalSubcommand(args: string): { sub: GoalSubcommand | undefined; rest: string } {
|
|
202
220
|
const trimmed = args.trim();
|
|
@@ -210,6 +228,10 @@ function parseGoalSubcommand(args: string): { sub: GoalSubcommand | undefined; r
|
|
|
210
228
|
return { sub: undefined, rest: trimmed };
|
|
211
229
|
}
|
|
212
230
|
|
|
231
|
+
function formatContextTokenCount(value: number): string {
|
|
232
|
+
return formatNumber(Math.max(0, Math.round(value))).toLowerCase();
|
|
233
|
+
}
|
|
234
|
+
|
|
213
235
|
/** Options for creating an InteractiveMode instance (for future API use) */
|
|
214
236
|
export interface InteractiveModeOptions {
|
|
215
237
|
/** Providers that were migrated during startup */
|
|
@@ -259,6 +281,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
259
281
|
loopPrompt: string | undefined = undefined;
|
|
260
282
|
loopLimit: LoopLimitRuntime | undefined = undefined;
|
|
261
283
|
#loopAutoSubmitTimer: NodeJS.Timeout | undefined;
|
|
284
|
+
#todoAutoClearTimer: NodeJS.Timeout | undefined;
|
|
262
285
|
todoPhases: TodoPhase[] = [];
|
|
263
286
|
hideThinkingBlock = false;
|
|
264
287
|
pendingImages: ImageContent[] = [];
|
|
@@ -391,6 +414,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
391
414
|
try {
|
|
392
415
|
this.historyStorage = HistoryStorage.open();
|
|
393
416
|
this.editor.setHistoryStorage(this.historyStorage);
|
|
417
|
+
this.historyStorage.setSessionResolver(() => this.sessionManager.getSessionId());
|
|
394
418
|
} catch (error) {
|
|
395
419
|
logger.warn("History storage unavailable", { error: String(error) });
|
|
396
420
|
}
|
|
@@ -554,6 +578,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
554
578
|
// subagent is doing the work for a still-pending todo) updates as
|
|
555
579
|
// subagents start, finish, or fail.
|
|
556
580
|
this.#reconcileTodosWithSubagents();
|
|
581
|
+
this.#syncTodoAutoClearTimer();
|
|
557
582
|
this.#renderTodoList();
|
|
558
583
|
this.ui.requestRender();
|
|
559
584
|
});
|
|
@@ -640,6 +665,31 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
640
665
|
this.session.setSlashCommands(fileCommands);
|
|
641
666
|
}
|
|
642
667
|
|
|
668
|
+
/**
|
|
669
|
+
* Re-point the process and every cwd-derived cache at `newCwd` after the
|
|
670
|
+
* active session's working directory changed (`/move` relocation or resuming
|
|
671
|
+
* a session from another project). The SessionManager's cwd MUST already
|
|
672
|
+
* reflect `newCwd` before this is called.
|
|
673
|
+
*/
|
|
674
|
+
async applyCwdChange(newCwd: string): Promise<void> {
|
|
675
|
+
setProjectDir(newCwd);
|
|
676
|
+
// Re-scope project settings (`.claude/settings.yml` etc.) to the new
|
|
677
|
+
// directory in place so the active session and every settings reader pick
|
|
678
|
+
// up the destination project's configuration.
|
|
679
|
+
if (isSettingsInitialized()) {
|
|
680
|
+
await settings.reloadForCwd(newCwd);
|
|
681
|
+
}
|
|
682
|
+
// Re-warm plugin roots, capabilities, slash commands, and the ssh tool so
|
|
683
|
+
// the next prompt sees everything scoped to the new project directory.
|
|
684
|
+
clearClaudePluginRootsCache();
|
|
685
|
+
resetCapabilities();
|
|
686
|
+
await this.refreshSlashCommandState(newCwd);
|
|
687
|
+
await this.session.refreshSshTool({ activateIfAvailable: true });
|
|
688
|
+
setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
|
|
689
|
+
this.statusLine.invalidate();
|
|
690
|
+
this.updateEditorTopBorder();
|
|
691
|
+
}
|
|
692
|
+
|
|
643
693
|
async getUserInput(): Promise<SubmittedUserInput> {
|
|
644
694
|
if (this.session.getGoalModeState()?.mode === "exiting") {
|
|
645
695
|
await this.#exitGoalMode({ reason: "completed", silent: true });
|
|
@@ -1042,6 +1092,47 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1042
1092
|
this.session.setTodoPhases(next);
|
|
1043
1093
|
}
|
|
1044
1094
|
|
|
1095
|
+
#cancelTodoAutoClearTimer(): void {
|
|
1096
|
+
if (!this.#todoAutoClearTimer) return;
|
|
1097
|
+
clearTimeout(this.#todoAutoClearTimer);
|
|
1098
|
+
this.#todoAutoClearTimer = undefined;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
#isClosedTodo(task: TodoItem): boolean {
|
|
1102
|
+
return task.status === "completed" || task.status === "abandoned";
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
#hasClosedTodos(phases: TodoPhase[]): boolean {
|
|
1106
|
+
return phases.some(phase => phase.tasks.some(task => this.#isClosedTodo(task)));
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
#removeClosedTodos(phases: TodoPhase[]): TodoPhase[] {
|
|
1110
|
+
const next: TodoPhase[] = [];
|
|
1111
|
+
for (const phase of phases) {
|
|
1112
|
+
const tasks = phase.tasks.filter(task => !this.#isClosedTodo(task));
|
|
1113
|
+
if (tasks.length > 0) next.push({ name: phase.name, tasks });
|
|
1114
|
+
}
|
|
1115
|
+
return next;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
#syncTodoAutoClearTimer(): void {
|
|
1119
|
+
this.#cancelTodoAutoClearTimer();
|
|
1120
|
+
const delaySeconds = this.settings.get("tasks.todoClearDelay");
|
|
1121
|
+
if (!Number.isFinite(delaySeconds) || delaySeconds < 0 || !this.#hasClosedTodos(this.todoPhases)) return;
|
|
1122
|
+
if (delaySeconds === 0) {
|
|
1123
|
+
this.todoPhases = this.#removeClosedTodos(this.todoPhases);
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
this.#todoAutoClearTimer = setTimeout(() => {
|
|
1128
|
+
this.#todoAutoClearTimer = undefined;
|
|
1129
|
+
this.todoPhases = this.#removeClosedTodos(this.todoPhases);
|
|
1130
|
+
this.#renderTodoList();
|
|
1131
|
+
this.ui.requestRender();
|
|
1132
|
+
}, delaySeconds * 1000);
|
|
1133
|
+
this.#todoAutoClearTimer.unref?.();
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1045
1136
|
#getActivePhase(phases: TodoPhase[]): TodoPhase | undefined {
|
|
1046
1137
|
const nonEmpty = phases.filter(phase => phase.tasks.length > 0);
|
|
1047
1138
|
const active = nonEmpty.find(phase =>
|
|
@@ -1097,6 +1188,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1097
1188
|
|
|
1098
1189
|
async #loadTodoList(): Promise<void> {
|
|
1099
1190
|
this.todoPhases = this.session.getTodoPhases();
|
|
1191
|
+
this.#syncTodoAutoClearTimer();
|
|
1100
1192
|
this.#renderTodoList();
|
|
1101
1193
|
}
|
|
1102
1194
|
|
|
@@ -1568,6 +1660,28 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1568
1660
|
return `up/down navigate enter select ${externalEditorKey.toLowerCase()} open in editor esc cancel`;
|
|
1569
1661
|
}
|
|
1570
1662
|
|
|
1663
|
+
#getPlanApprovalContextUsage(): ContextUsage | undefined {
|
|
1664
|
+
const executionModel = this.#planModePreviousModelState?.model ?? this.session.model;
|
|
1665
|
+
const contextWindow = executionModel?.contextWindow;
|
|
1666
|
+
if (typeof contextWindow === "number") {
|
|
1667
|
+
return this.session.getContextUsage({ contextWindow });
|
|
1668
|
+
}
|
|
1669
|
+
return this.session.getContextUsage();
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
#formatKeepContextLabel(contextUsage: ContextUsage | undefined): string {
|
|
1673
|
+
if (contextUsage?.tokens == null) {
|
|
1674
|
+
return "Approve and keep context";
|
|
1675
|
+
}
|
|
1676
|
+
const tokens = formatContextTokenCount(contextUsage.tokens);
|
|
1677
|
+
const contextWindow = formatContextTokenCount(contextUsage.contextWindow);
|
|
1678
|
+
return `Approve and keep context (~${tokens} / ${contextWindow})`;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
#isKeepContextDisabled(contextUsage: ContextUsage | undefined): boolean {
|
|
1682
|
+
return contextUsage?.percent != null && contextUsage.percent > PLAN_KEEP_CONTEXT_DISABLE_THRESHOLD_PERCENT;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1571
1685
|
async #openPlanInExternalEditor(planFilePath: string): Promise<void> {
|
|
1572
1686
|
const editorCmd = getEditorCommand();
|
|
1573
1687
|
if (!editorCmd) {
|
|
@@ -2034,11 +2148,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2034
2148
|
}
|
|
2035
2149
|
|
|
2036
2150
|
this.#renderPlanPreview(planContent, { append: true });
|
|
2037
|
-
const contextUsage = this
|
|
2038
|
-
const keepContextLabel =
|
|
2039
|
-
|
|
2040
|
-
? `Approve and keep context (${contextUsage.percent.toFixed(1)}%)`
|
|
2041
|
-
: "Approve and keep context";
|
|
2151
|
+
const contextUsage = this.#getPlanApprovalContextUsage();
|
|
2152
|
+
const keepContextLabel = this.#formatKeepContextLabel(contextUsage);
|
|
2153
|
+
const keepContextDisabled = this.#isKeepContextDisabled(contextUsage);
|
|
2042
2154
|
|
|
2043
2155
|
// Model-tier slider: let the operator pick which configured role model
|
|
2044
2156
|
// (smol/default/slow/…) executes the approved plan. The slider always starts
|
|
@@ -2073,6 +2185,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2073
2185
|
{
|
|
2074
2186
|
helpText,
|
|
2075
2187
|
onExternalEditor: () => void this.#openPlanInExternalEditor(planFilePath),
|
|
2188
|
+
disabledIndices: keepContextDisabled ? [PLAN_KEEP_CONTEXT_OPTION_INDEX] : undefined,
|
|
2076
2189
|
},
|
|
2077
2190
|
{ slider },
|
|
2078
2191
|
);
|
|
@@ -2175,6 +2288,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2175
2288
|
this.loadingAnimation = undefined;
|
|
2176
2289
|
}
|
|
2177
2290
|
this.#cleanupMicAnimation();
|
|
2291
|
+
this.#cancelTodoAutoClearTimer();
|
|
2178
2292
|
this.#cancelGoalContinuation();
|
|
2179
2293
|
if (this.#sttController) {
|
|
2180
2294
|
this.#sttController.dispose();
|
|
@@ -2230,14 +2344,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2230
2344
|
// Emit shutdown event to hooks
|
|
2231
2345
|
await this.session.dispose();
|
|
2232
2346
|
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
// Wait for any pending renders to complete
|
|
2238
|
-
// requestRender() uses process.nextTick(), so we wait one tick
|
|
2239
|
-
await new Promise(resolve => process.nextTick(resolve));
|
|
2240
|
-
|
|
2347
|
+
// Do not force a final render during teardown: disposed session/UI state can
|
|
2348
|
+
// collapse to an empty frame, clearing the viewport and leaving the parent
|
|
2349
|
+
// shell prompt at row 0. Stop from the last committed frame so the terminal
|
|
2350
|
+
// hands Bash the cursor immediately after visible OMP content.
|
|
2241
2351
|
// Drain any in-flight Kitty key release events before stopping.
|
|
2242
2352
|
// This prevents escape sequences from leaking to the parent shell over slow SSH.
|
|
2243
2353
|
await this.ui.terminal.drainInput(1000);
|
|
@@ -2857,6 +2967,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2857
2967
|
},
|
|
2858
2968
|
];
|
|
2859
2969
|
}
|
|
2970
|
+
this.#syncTodoAutoClearTimer();
|
|
2860
2971
|
this.#renderTodoList();
|
|
2861
2972
|
this.ui.requestRender();
|
|
2862
2973
|
}
|
|
@@ -2896,8 +3007,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2896
3007
|
|
|
2897
3008
|
showHookSelector(
|
|
2898
3009
|
title: string,
|
|
2899
|
-
options:
|
|
2900
|
-
dialogOptions?:
|
|
3010
|
+
options: ExtensionUISelectItem[],
|
|
3011
|
+
dialogOptions?: InteractiveSelectorDialogOptions,
|
|
2901
3012
|
extra?: { slider?: HookSelectorSlider },
|
|
2902
3013
|
): Promise<string | undefined> {
|
|
2903
3014
|
return this.#extensionUiController.showHookSelector(title, options, dialogOptions, extra);
|
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { getOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
|
|
14
14
|
import { $env, readJsonl, Snowflake } from "@oh-my-pi/pi-utils";
|
|
15
|
-
import
|
|
16
|
-
ExtensionUIContext,
|
|
17
|
-
ExtensionUIDialogOptions,
|
|
18
|
-
|
|
15
|
+
import {
|
|
16
|
+
type ExtensionUIContext,
|
|
17
|
+
type ExtensionUIDialogOptions,
|
|
18
|
+
type ExtensionUISelectItem,
|
|
19
|
+
type ExtensionWidgetOptions,
|
|
20
|
+
getExtensionUISelectOptionLabel,
|
|
19
21
|
} from "../../extensibility/extensions";
|
|
20
22
|
import { type Theme, theme } from "../../modes/theme/theme";
|
|
21
23
|
import type { AgentSession } from "../../session/agent-session";
|
|
@@ -256,11 +258,20 @@ export async function runRpcMode(
|
|
|
256
258
|
return promise;
|
|
257
259
|
}
|
|
258
260
|
|
|
259
|
-
select(
|
|
261
|
+
select(
|
|
262
|
+
title: string,
|
|
263
|
+
options: ExtensionUISelectItem[],
|
|
264
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
265
|
+
): Promise<string | undefined> {
|
|
260
266
|
return this.#createDialogPromise(
|
|
261
267
|
dialogOptions,
|
|
262
268
|
undefined,
|
|
263
|
-
{
|
|
269
|
+
{
|
|
270
|
+
method: "select",
|
|
271
|
+
title,
|
|
272
|
+
options: options.map(getExtensionUISelectOptionLabel),
|
|
273
|
+
timeout: dialogOptions?.timeout,
|
|
274
|
+
},
|
|
264
275
|
response => parseValueDialogResponse(response, dialogOptions),
|
|
265
276
|
);
|
|
266
277
|
}
|
|
@@ -404,6 +404,36 @@
|
|
|
404
404
|
"additionalProperties": {
|
|
405
405
|
"type": "string"
|
|
406
406
|
}
|
|
407
|
+
},
|
|
408
|
+
"spinnerFrames": {
|
|
409
|
+
"description": "Override the spinner frames. Use a flat array to set both `status` and `activity`, or an object to override each independently. Frames are advanced ~12.5fps for status spinners and ~60fps for activity spinners.",
|
|
410
|
+
"oneOf": [
|
|
411
|
+
{
|
|
412
|
+
"type": "array",
|
|
413
|
+
"minItems": 1,
|
|
414
|
+
"items": { "type": "string", "minLength": 1 }
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
"type": "object",
|
|
418
|
+
"properties": {
|
|
419
|
+
"status": {
|
|
420
|
+
"type": "array",
|
|
421
|
+
"minItems": 1,
|
|
422
|
+
"items": { "type": "string", "minLength": 1 }
|
|
423
|
+
},
|
|
424
|
+
"activity": {
|
|
425
|
+
"type": "array",
|
|
426
|
+
"minItems": 1,
|
|
427
|
+
"items": { "type": "string", "minLength": 1 }
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
"additionalProperties": false,
|
|
431
|
+
"anyOf": [
|
|
432
|
+
{ "required": ["status"] },
|
|
433
|
+
{ "required": ["activity"] }
|
|
434
|
+
]
|
|
435
|
+
}
|
|
436
|
+
]
|
|
407
437
|
}
|
|
408
438
|
},
|
|
409
439
|
"additionalProperties": false
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -812,6 +812,25 @@ const SPINNER_FRAMES: Record<SymbolPreset, Record<SpinnerType, string[]>> = {
|
|
|
812
812
|
},
|
|
813
813
|
};
|
|
814
814
|
|
|
815
|
+
/**
|
|
816
|
+
* Shape accepted by `themeJson.symbols.spinnerFrames`. A flat array applies to
|
|
817
|
+
* both spinner types; an object lets a theme override `status` and/or
|
|
818
|
+
* `activity` independently. Anything not specified falls back to the symbol
|
|
819
|
+
* preset's default frames.
|
|
820
|
+
*/
|
|
821
|
+
type SpinnerFramesOverride = string[] | { status?: string[]; activity?: string[] };
|
|
822
|
+
|
|
823
|
+
function normalizeSpinnerFramesOverride(
|
|
824
|
+
value: SpinnerFramesOverride | undefined,
|
|
825
|
+
): Partial<Record<SpinnerType, string[]>> {
|
|
826
|
+
if (value === undefined) return {};
|
|
827
|
+
if (Array.isArray(value)) return { status: value, activity: value };
|
|
828
|
+
const result: Partial<Record<SpinnerType, string[]>> = {};
|
|
829
|
+
if (value.status) result.status = value.status;
|
|
830
|
+
if (value.activity) result.activity = value.activity;
|
|
831
|
+
return result;
|
|
832
|
+
}
|
|
833
|
+
|
|
815
834
|
// ============================================================================
|
|
816
835
|
// Types & Schema
|
|
817
836
|
// ============================================================================
|
|
@@ -898,6 +917,19 @@ const themeColorsSchema = z.object(
|
|
|
898
917
|
},
|
|
899
918
|
);
|
|
900
919
|
|
|
920
|
+
const spinnerFramesArraySchema = z.array(z.string().min(1)).min(1);
|
|
921
|
+
const spinnerFramesSchema = z.union([
|
|
922
|
+
spinnerFramesArraySchema,
|
|
923
|
+
z
|
|
924
|
+
.object({
|
|
925
|
+
status: spinnerFramesArraySchema.optional(),
|
|
926
|
+
activity: spinnerFramesArraySchema.optional(),
|
|
927
|
+
})
|
|
928
|
+
.refine(value => value.status !== undefined || value.activity !== undefined, {
|
|
929
|
+
message: "spinnerFrames object must define `status` and/or `activity`",
|
|
930
|
+
}),
|
|
931
|
+
]);
|
|
932
|
+
|
|
901
933
|
const symbolPresetSchema = z.enum(["unicode", "nerd", "ascii"]);
|
|
902
934
|
|
|
903
935
|
const themeJsonSchema = z.object({
|
|
@@ -916,6 +948,7 @@ const themeJsonSchema = z.object({
|
|
|
916
948
|
.object({
|
|
917
949
|
preset: symbolPresetSchema.optional(),
|
|
918
950
|
overrides: z.record(z.string(), z.string()).optional(),
|
|
951
|
+
spinnerFrames: spinnerFramesSchema.optional(),
|
|
919
952
|
})
|
|
920
953
|
.optional(),
|
|
921
954
|
});
|
|
@@ -1243,6 +1276,7 @@ export class Theme {
|
|
|
1243
1276
|
#fgColors: Record<ThemeColor, string>;
|
|
1244
1277
|
#bgColors: Record<ThemeBg, string>;
|
|
1245
1278
|
#symbols: SymbolMap;
|
|
1279
|
+
#spinnerFramesOverrides: Partial<Record<SpinnerType, string[]>>;
|
|
1246
1280
|
|
|
1247
1281
|
constructor(
|
|
1248
1282
|
fgColors: Record<ThemeColor, string | number>,
|
|
@@ -1250,6 +1284,7 @@ export class Theme {
|
|
|
1250
1284
|
private readonly mode: ColorMode,
|
|
1251
1285
|
private readonly symbolPreset: SymbolPreset,
|
|
1252
1286
|
symbolOverrides: Partial<Record<SymbolKey, string>>,
|
|
1287
|
+
spinnerFramesOverrides: Partial<Record<SpinnerType, string[]>> = {},
|
|
1253
1288
|
) {
|
|
1254
1289
|
this.#fgColors = {} as Record<ThemeColor, string>;
|
|
1255
1290
|
for (const [key, value] of Object.entries(fgColors) as [ThemeColor, string | number][]) {
|
|
@@ -1269,6 +1304,7 @@ export class Theme {
|
|
|
1269
1304
|
logger.debug("Invalid symbol key in override", { key, availableKeys: Object.keys(this.#symbols) });
|
|
1270
1305
|
}
|
|
1271
1306
|
}
|
|
1307
|
+
this.#spinnerFramesOverrides = spinnerFramesOverrides;
|
|
1272
1308
|
}
|
|
1273
1309
|
|
|
1274
1310
|
fg(color: ThemeColor, text: string): string {
|
|
@@ -1563,7 +1599,7 @@ export class Theme {
|
|
|
1563
1599
|
* Get spinner frames by type.
|
|
1564
1600
|
*/
|
|
1565
1601
|
getSpinnerFrames(type: SpinnerType = "status"): string[] {
|
|
1566
|
-
return SPINNER_FRAMES[this.symbolPreset][type];
|
|
1602
|
+
return this.#spinnerFramesOverrides[type] ?? SPINNER_FRAMES[this.symbolPreset][type];
|
|
1567
1603
|
}
|
|
1568
1604
|
|
|
1569
1605
|
/**
|
|
@@ -1735,7 +1771,8 @@ function createTheme(themeJson: ThemeJson, options: CreateThemeOptions = {}): Th
|
|
|
1735
1771
|
// Extract symbol configuration - settings override takes precedence over theme
|
|
1736
1772
|
const symbolPreset: SymbolPreset = symbolPresetOverride ?? themeJson.symbols?.preset ?? "unicode";
|
|
1737
1773
|
const symbolOverrides = themeJson.symbols?.overrides ?? {};
|
|
1738
|
-
|
|
1774
|
+
const spinnerFramesOverrides = normalizeSpinnerFramesOverride(themeJson.symbols?.spinnerFrames);
|
|
1775
|
+
return new Theme(fgColors, bgColors, colorMode, symbolPreset, symbolOverrides, spinnerFramesOverrides);
|
|
1739
1776
|
}
|
|
1740
1777
|
|
|
1741
1778
|
async function loadTheme(name: string, options: CreateThemeOptions = {}): Promise<Theme> {
|
package/src/modes/types.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { Settings } from "../config/settings";
|
|
|
7
7
|
import type {
|
|
8
8
|
ExtensionUIContext,
|
|
9
9
|
ExtensionUIDialogOptions,
|
|
10
|
+
ExtensionUISelectItem,
|
|
10
11
|
ExtensionWidgetContent,
|
|
11
12
|
ExtensionWidgetOptions,
|
|
12
13
|
} from "../extensibility/extensions";
|
|
@@ -24,7 +25,7 @@ import type { CustomEditor } from "./components/custom-editor";
|
|
|
24
25
|
import type { EvalExecutionComponent } from "./components/eval-execution";
|
|
25
26
|
import type { HookEditorComponent } from "./components/hook-editor";
|
|
26
27
|
import type { HookInputComponent } from "./components/hook-input";
|
|
27
|
-
import type { HookSelectorComponent } from "./components/hook-selector";
|
|
28
|
+
import type { HookSelectorComponent, HookSelectorOptions } from "./components/hook-selector";
|
|
28
29
|
import type { StatusLineComponent } from "./components/status-line";
|
|
29
30
|
import type { ToolExecutionHandle } from "./components/tool-execution";
|
|
30
31
|
import type { LoopLimitRuntime } from "./loop-limit";
|
|
@@ -63,6 +64,8 @@ export interface InteractiveModeInitOptions {
|
|
|
63
64
|
suppressWelcomeIntro?: boolean;
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
export type InteractiveSelectorDialogOptions = ExtensionUIDialogOptions & Pick<HookSelectorOptions, "disabledIndices">;
|
|
68
|
+
|
|
66
69
|
export interface InteractiveModeContext {
|
|
67
70
|
// UI access
|
|
68
71
|
ui: TUI;
|
|
@@ -241,6 +244,7 @@ export interface InteractiveModeContext {
|
|
|
241
244
|
): Promise<CompactionOutcome>;
|
|
242
245
|
openInBrowser(urlOrPath: string): void;
|
|
243
246
|
refreshSlashCommandState(cwd?: string): Promise<void>;
|
|
247
|
+
applyCwdChange(newCwd: string): Promise<void>;
|
|
244
248
|
|
|
245
249
|
// Selector handling
|
|
246
250
|
showSettingsSelector(): void;
|
|
@@ -297,8 +301,8 @@ export interface InteractiveModeContext {
|
|
|
297
301
|
setHookStatus(key: string, text: string | undefined): void;
|
|
298
302
|
showHookSelector(
|
|
299
303
|
title: string,
|
|
300
|
-
options:
|
|
301
|
-
dialogOptions?:
|
|
304
|
+
options: ExtensionUISelectItem[],
|
|
305
|
+
dialogOptions?: InteractiveSelectorDialogOptions,
|
|
302
306
|
): Promise<string | undefined>;
|
|
303
307
|
hideHookSelector(): void;
|
|
304
308
|
showHookInput(title: string, placeholder?: string): Promise<string | undefined>;
|
|
@@ -262,8 +262,11 @@ export class UiHelpers {
|
|
|
262
262
|
break;
|
|
263
263
|
}
|
|
264
264
|
case "assistant": {
|
|
265
|
-
const assistantComponent = new AssistantMessageComponent(
|
|
266
|
-
|
|
265
|
+
const assistantComponent = new AssistantMessageComponent(
|
|
266
|
+
message,
|
|
267
|
+
this.ctx.hideThinkingBlock,
|
|
268
|
+
() => this.ctx.ui.requestRender(),
|
|
269
|
+
this.ctx.session.extensionRunner?.getAssistantThinkingRenderers(),
|
|
267
270
|
);
|
|
268
271
|
this.ctx.chatContainer.addChild(assistantComponent);
|
|
269
272
|
break;
|