@gajae-code/coding-agent 0.12.1 → 0.12.2
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 +30 -0
- package/dist/types/config/settings-schema.d.ts +2 -2
- package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -3
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +1 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +9 -0
- package/dist/types/modes/theme/theme.d.ts +3 -1
- package/dist/types/modes/types.d.ts +32 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -2
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +22 -3
- package/dist/types/task/index.d.ts +4 -0
- package/dist/types/task/output-manager.d.ts +1 -0
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/tools/index.d.ts +2 -0
- package/package.json +8 -8
- package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/run-acp-conformance.ts +5 -0
- package/src/async/job-manager.ts +22 -20
- package/src/config/settings-schema.ts +2 -2
- package/src/debug/index.ts +42 -17
- package/src/defaults/gjc/skills/ralplan/SKILL.md +9 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -2
- package/src/gjc-runtime/launch-worktree.ts +103 -1
- package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
- package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
- package/src/gjc-runtime/ralplan-runtime.ts +80 -10
- package/src/gjc-runtime/ultragoal-evidence.ts +6 -74
- package/src/gjc-runtime/ultragoal-runtime.ts +236 -3
- package/src/gjc-runtime/workflow-manifest.generated.json +29 -0
- package/src/gjc-runtime/workflow-manifest.ts +17 -2
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/modes/DESIGN.md +53 -0
- package/src/modes/components/irc-sidebar.ts +30 -1
- package/src/modes/components/tool-status-header.ts +0 -1
- package/src/modes/controllers/command-controller.ts +38 -34
- package/src/modes/controllers/event-controller.ts +27 -16
- package/src/modes/controllers/extension-ui-controller.ts +186 -34
- package/src/modes/controllers/selector-controller.ts +115 -22
- package/src/modes/interactive-mode.ts +189 -62
- package/src/modes/jobs-observer.ts +1 -1
- package/src/modes/theme/theme.ts +11 -9
- package/src/modes/types.ts +88 -0
- package/src/sdk/broker/lifecycle.ts +18 -13
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -2
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +1 -0
- package/src/session/agent-session.ts +22 -1
- package/src/session/internal/managed-session-scope.ts +158 -107
- package/src/session/internal/managed-session-storage.ts +445 -57
- package/src/session/session-storage.ts +29 -44
- package/src/task/index.ts +257 -31
- package/src/task/output-manager.ts +24 -14
- package/src/task/receipt.ts +11 -1
- package/src/tools/index.ts +2 -0
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
Text,
|
|
14
14
|
TUI,
|
|
15
15
|
} from "@gajae-code/tui";
|
|
16
|
-
import { APP_NAME, adjustHsv, getProjectDir, logger, postmortem } from "@gajae-code/utils";
|
|
16
|
+
import { APP_NAME, adjustHsv, getProjectDir, logger, postmortem, sanitizeText } from "@gajae-code/utils";
|
|
17
17
|
import chalk from "chalk";
|
|
18
18
|
import { AsyncJobManager } from "../async";
|
|
19
19
|
import {
|
|
@@ -61,7 +61,12 @@ import { GajaePetWidget, type PetMode } from "./components/gajae-pet-widget";
|
|
|
61
61
|
import type { HookEditorComponent } from "./components/hook-editor";
|
|
62
62
|
import type { HookInputComponent } from "./components/hook-input";
|
|
63
63
|
import type { HookSelectorComponent } from "./components/hook-selector";
|
|
64
|
-
import {
|
|
64
|
+
import {
|
|
65
|
+
computeIrcSplitWidths,
|
|
66
|
+
getIrcSidebarSemanticToken,
|
|
67
|
+
IrcLeftLaneComponent,
|
|
68
|
+
IrcSplitViewComponent,
|
|
69
|
+
} from "./components/irc-sidebar";
|
|
65
70
|
import {
|
|
66
71
|
getPetUnavailableWarning,
|
|
67
72
|
isPetAvailable,
|
|
@@ -118,8 +123,9 @@ import { addChatChild, prepareTranscriptRebuild, UiHelpers } from "./utils/ui-he
|
|
|
118
123
|
function buildComposerPlaceholder(
|
|
119
124
|
keybindings: Pick<KeybindingsManager, "getDisplayString">,
|
|
120
125
|
context: KeyDisplayContext,
|
|
121
|
-
options: { readonly busy: boolean; readonly busyPromptMode: "steer" | "queue" },
|
|
126
|
+
options: { readonly busy: boolean; readonly busyPromptMode: "steer" | "queue"; readonly showActionHints: boolean },
|
|
122
127
|
): string {
|
|
128
|
+
if (!options.showActionHints) return "Type your message...";
|
|
123
129
|
const parts: string[] = [];
|
|
124
130
|
const submitKey = options.busy ? keybindings.getDisplayString("tui.input.submit", context) : "";
|
|
125
131
|
if (submitKey) {
|
|
@@ -155,7 +161,11 @@ export function getDefaultComposerPlaceholder(
|
|
|
155
161
|
KeybindingsManager.inMemory({
|
|
156
162
|
"app.message.queue": defaultMessageQueueKeysForPlatform(context.platform),
|
|
157
163
|
});
|
|
158
|
-
return buildComposerPlaceholder(effectiveKeybindings, context, {
|
|
164
|
+
return buildComposerPlaceholder(effectiveKeybindings, context, {
|
|
165
|
+
busy: false,
|
|
166
|
+
busyPromptMode: "steer",
|
|
167
|
+
showActionHints: true,
|
|
168
|
+
});
|
|
159
169
|
}
|
|
160
170
|
|
|
161
171
|
export const DEFAULT_COMPOSER_PLACEHOLDER = getDefaultComposerPlaceholder();
|
|
@@ -163,10 +173,24 @@ export const DEFAULT_COMPOSER_PLACEHOLDER = getDefaultComposerPlaceholder();
|
|
|
163
173
|
export function getComposerPlaceholder(
|
|
164
174
|
keybindings: Pick<KeybindingsManager, "getDisplayString">,
|
|
165
175
|
context: KeyDisplayContext,
|
|
166
|
-
options: { readonly busy: boolean; readonly busyPromptMode: "steer" | "queue" },
|
|
176
|
+
options: { readonly busy: boolean; readonly busyPromptMode: "steer" | "queue"; readonly showActionHints: boolean },
|
|
167
177
|
): string {
|
|
168
178
|
return buildComposerPlaceholder(keybindings, context, options);
|
|
169
179
|
}
|
|
180
|
+
|
|
181
|
+
export function resolveActivityIndicatorMessage(
|
|
182
|
+
foregroundActive: boolean,
|
|
183
|
+
activeBackgroundTasks: number,
|
|
184
|
+
foregroundMessage: string,
|
|
185
|
+
): string | undefined {
|
|
186
|
+
const backgroundCount = Math.max(0, Math.trunc(activeBackgroundTasks));
|
|
187
|
+
if (foregroundActive) {
|
|
188
|
+
if (backgroundCount === 0) return foregroundMessage;
|
|
189
|
+
return `${foregroundMessage} · ${backgroundCount} background task${backgroundCount === 1 ? "" : "s"}`;
|
|
190
|
+
}
|
|
191
|
+
if (backgroundCount === 0) return undefined;
|
|
192
|
+
return `Background: ${backgroundCount} task${backgroundCount === 1 ? "" : "s"}…`;
|
|
193
|
+
}
|
|
170
194
|
const WELCOME_RESERVED_CONTAINER_CHILD_LIMIT = 8;
|
|
171
195
|
const COMPOSER_RIGHT_GUTTER_WIDTH = 1;
|
|
172
196
|
|
|
@@ -432,6 +456,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
432
456
|
|
|
433
457
|
#jobsObserver?: JobsObserver;
|
|
434
458
|
#tasksAggregator?: TasksAggregator;
|
|
459
|
+
#foregroundActivity = false;
|
|
460
|
+
#activityIndicatorSuspensions = 0;
|
|
461
|
+
#suspendedActivityIndicator?: Loader;
|
|
462
|
+
#stopped = false;
|
|
463
|
+
#initPromise?: Promise<void>;
|
|
464
|
+
#stopListeners = new Set<() => void>();
|
|
435
465
|
#eventBus?: EventBus;
|
|
436
466
|
#eventBusUnsubscribers: Array<() => void> = [];
|
|
437
467
|
#welcomeComponent?: WelcomeComponent;
|
|
@@ -646,7 +676,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
646
676
|
this.#todoCommandController = new TodoCommandController(this);
|
|
647
677
|
this.#selectorController = new SelectorController(this);
|
|
648
678
|
this.#inputController = new InputController(this);
|
|
649
|
-
|
|
679
|
+
// Composer shortcut discovery owns contextual action hints; retain only status telemetry in the rail.
|
|
650
680
|
this.promptSuggestion = new PromptSuggestionController(this);
|
|
651
681
|
this.#observerRegistry = new SessionObserverRegistry();
|
|
652
682
|
}
|
|
@@ -664,8 +694,16 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
664
694
|
);
|
|
665
695
|
}
|
|
666
696
|
|
|
667
|
-
|
|
668
|
-
if (this.isInitialized) return;
|
|
697
|
+
init(): Promise<void> {
|
|
698
|
+
if (this.#stopped || this.isInitialized) return Promise.resolve();
|
|
699
|
+
this.#initPromise ??= this.#initialize().finally(() => {
|
|
700
|
+
this.#initPromise = undefined;
|
|
701
|
+
});
|
|
702
|
+
return this.#initPromise;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
async #initialize(): Promise<void> {
|
|
706
|
+
if (this.#stopped || this.isInitialized) return;
|
|
669
707
|
|
|
670
708
|
this.keybindings = logger.time("InteractiveMode.init:keybindings", () => KeybindingsManager.create());
|
|
671
709
|
this.keybindings.setDisplayContext(this.#keyDisplayContext);
|
|
@@ -686,6 +724,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
686
724
|
this.refreshSlashCommandState.bind(this),
|
|
687
725
|
getProjectDir(),
|
|
688
726
|
);
|
|
727
|
+
if (this.#stopped) return;
|
|
689
728
|
|
|
690
729
|
// Get current model info for welcome screen
|
|
691
730
|
const modelName = this.session.model?.name ?? "Unknown";
|
|
@@ -814,10 +853,15 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
814
853
|
this.#observerRegistry,
|
|
815
854
|
this.session.getAgentId(),
|
|
816
855
|
);
|
|
856
|
+
this.#tasksAggregator.onChange(() => {
|
|
857
|
+
this.syncActivityIndicator();
|
|
858
|
+
this.ui.requestRender();
|
|
859
|
+
});
|
|
817
860
|
}
|
|
818
861
|
|
|
819
862
|
// Load initial todos
|
|
820
863
|
await this.#loadTodoList();
|
|
864
|
+
if (this.#stopped) return;
|
|
821
865
|
|
|
822
866
|
// Start the UI
|
|
823
867
|
this.ui.start();
|
|
@@ -826,6 +870,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
826
870
|
this.updateEditorChrome();
|
|
827
871
|
this.#syncEditorMaxHeight();
|
|
828
872
|
this.isInitialized = true;
|
|
873
|
+
this.syncActivityIndicator();
|
|
829
874
|
if (this.settings.get("tasksPane.defaultVisible")) this.showTasksPane();
|
|
830
875
|
this.#syncIrcSidebarAvailabilityFromSettings();
|
|
831
876
|
this.ui.requestRender(true);
|
|
@@ -837,6 +882,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
837
882
|
getRecentSessions(this.sessionManager.getSessionDir()),
|
|
838
883
|
)
|
|
839
884
|
.then(sessions => {
|
|
885
|
+
if (this.#stopped) return;
|
|
840
886
|
if (this.#welcomeComponent !== welcomeComponent) return;
|
|
841
887
|
welcomeComponent.setRecentSessions(
|
|
842
888
|
sessions.map(session => ({
|
|
@@ -871,22 +917,26 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
871
917
|
}
|
|
872
918
|
if (starReminderGate.schedule) {
|
|
873
919
|
scheduleLaunchStarReminderAfterFirstRender({
|
|
874
|
-
confirm: (title, message) =>
|
|
920
|
+
confirm: (title, message) =>
|
|
921
|
+
this.#stopped ? Promise.resolve(false) : this.showHookConfirm(title, message),
|
|
875
922
|
isIdle: () => !this.session.isStreaming && !this.isBackgrounded && !this.hookSelector,
|
|
876
923
|
});
|
|
877
924
|
}
|
|
878
925
|
|
|
879
926
|
// Initialize hooks with TUI-based UI context
|
|
880
927
|
await this.initHooksAndCustomTools();
|
|
928
|
+
if (this.#stopped) return;
|
|
881
929
|
|
|
882
930
|
// Restore mode from session (e.g. plan mode on resume)
|
|
883
931
|
await this.#restoreModeFromSession();
|
|
932
|
+
if (this.#stopped) return;
|
|
884
933
|
|
|
885
934
|
// Restore unsent editor draft from previous session shutdown (Ctrl+D).
|
|
886
935
|
// One-shot: consumeDraft removes the sidecar after read so the next
|
|
887
936
|
// resume does not re-restore the same text.
|
|
888
937
|
try {
|
|
889
938
|
const draft = await this.sessionManager.consumeDraft();
|
|
939
|
+
if (this.#stopped) return;
|
|
890
940
|
if (draft && !this.editor.getText()) {
|
|
891
941
|
this.editor.setText(draft);
|
|
892
942
|
this.updateEditorChrome();
|
|
@@ -895,6 +945,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
895
945
|
} catch (err) {
|
|
896
946
|
logger.warn("Failed to restore session draft", { error: String(err) });
|
|
897
947
|
}
|
|
948
|
+
if (this.#stopped) return;
|
|
898
949
|
|
|
899
950
|
// Subscribe to agent events
|
|
900
951
|
this.#subscribeToAgent();
|
|
@@ -906,6 +957,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
906
957
|
);
|
|
907
958
|
// Set up theme file watcher
|
|
908
959
|
onThemeChange(() => {
|
|
960
|
+
if (this.#stopped) return;
|
|
909
961
|
clearRenderCache();
|
|
910
962
|
this.#ircSplitView.invalidateTheme();
|
|
911
963
|
configureDefaultComposerChrome(this.editor);
|
|
@@ -938,8 +990,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
938
990
|
|
|
939
991
|
/** Reload slash commands and autocomplete for the provided working directory. */
|
|
940
992
|
async refreshSlashCommandState(cwd?: string): Promise<void> {
|
|
993
|
+
if (this.#stopped) return;
|
|
941
994
|
const basePath = cwd ?? this.sessionManager.getCwd();
|
|
942
995
|
const fileCommands = await loadSlashCommands({ cwd: basePath });
|
|
996
|
+
if (this.#stopped) return;
|
|
943
997
|
const fileCommandNames = new Set(fileCommands.map(cmd => cmd.name));
|
|
944
998
|
this.fileSlashCommands = fileCommandNames;
|
|
945
999
|
const fileSlashCommands: SlashCommand[] = fileCommands.map(cmd => ({
|
|
@@ -1069,11 +1123,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1069
1123
|
this.#pendingSubmissionDispose = undefined;
|
|
1070
1124
|
this.#pendingWorkingMessage = undefined;
|
|
1071
1125
|
this.#goalModeController.onPendingSubmissionFinished(submission.customType);
|
|
1072
|
-
|
|
1073
|
-
this.loadingAnimation.stop();
|
|
1074
|
-
this.loadingAnimation = undefined;
|
|
1075
|
-
this.statusContainer.clear();
|
|
1076
|
-
}
|
|
1126
|
+
this.stopLoadingAnimation();
|
|
1077
1127
|
if (!submission.customType) {
|
|
1078
1128
|
this.pendingImages = submission.images ? [...submission.images] : [];
|
|
1079
1129
|
this.rebuildChatFromMessages("reconcile-same-transcript");
|
|
@@ -1105,11 +1155,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1105
1155
|
this.optimisticUserMessageSignature = undefined;
|
|
1106
1156
|
pendingSubmissionDispose?.();
|
|
1107
1157
|
this.#pendingWorkingMessage = undefined;
|
|
1108
|
-
|
|
1109
|
-
this.loadingAnimation.stop();
|
|
1110
|
-
this.loadingAnimation = undefined;
|
|
1111
|
-
this.statusContainer.clear();
|
|
1112
|
-
}
|
|
1158
|
+
this.stopLoadingAnimation();
|
|
1113
1159
|
}
|
|
1114
1160
|
}
|
|
1115
1161
|
|
|
@@ -1132,6 +1178,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1132
1178
|
return getComposerPlaceholder(this.keybindings, this.#keyDisplayContext, {
|
|
1133
1179
|
busy: this.#isPromptDeliveryBusy(),
|
|
1134
1180
|
busyPromptMode: this.settings.get("busyPromptMode"),
|
|
1181
|
+
showActionHints: this.settings.get("statusLine.showActionHints"),
|
|
1135
1182
|
});
|
|
1136
1183
|
}
|
|
1137
1184
|
|
|
@@ -1280,18 +1327,23 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1280
1327
|
this.renderSessionContext(context);
|
|
1281
1328
|
}
|
|
1282
1329
|
|
|
1330
|
+
#sanitizeTodoText(text: string): string {
|
|
1331
|
+
return sanitizeText(text).replaceAll("\t", " ");
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1283
1334
|
#formatTodoLine(todo: TodoItem, prefix: string): string {
|
|
1284
1335
|
const checkbox = theme.checkbox;
|
|
1285
1336
|
const marker = formatHudNoteMarker(todo.notes?.length ?? 0);
|
|
1337
|
+
const content = this.#sanitizeTodoText(todo.content);
|
|
1286
1338
|
switch (todo.status) {
|
|
1287
1339
|
case "completed":
|
|
1288
|
-
return theme.fg("success", `${prefix}${checkbox.checked} ${chalk.strikethrough(
|
|
1340
|
+
return theme.fg("success", `${prefix}${checkbox.checked} ${chalk.strikethrough(content)}`) + marker;
|
|
1289
1341
|
case "in_progress":
|
|
1290
|
-
return theme.fg("accent", `${prefix}${checkbox.unchecked} ${
|
|
1342
|
+
return theme.fg("accent", `${prefix}${checkbox.unchecked} ${content}`) + marker;
|
|
1291
1343
|
case "abandoned":
|
|
1292
|
-
return theme.fg("error", `${prefix}${checkbox.unchecked} ${chalk.strikethrough(
|
|
1344
|
+
return theme.fg("error", `${prefix}${checkbox.unchecked} ${chalk.strikethrough(content)}`) + marker;
|
|
1293
1345
|
default:
|
|
1294
|
-
return theme.fg("dim", `${prefix}${checkbox.unchecked} ${
|
|
1346
|
+
return theme.fg("dim", `${prefix}${checkbox.unchecked} ${content}`) + marker;
|
|
1295
1347
|
}
|
|
1296
1348
|
}
|
|
1297
1349
|
|
|
@@ -1303,6 +1355,13 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1303
1355
|
return active ?? nonEmpty[nonEmpty.length - 1];
|
|
1304
1356
|
}
|
|
1305
1357
|
|
|
1358
|
+
#addTodoContent(lines: string[]): void {
|
|
1359
|
+
const content = new Text(lines.join("\n"), 1, 0);
|
|
1360
|
+
this.todoContainer.addChild(
|
|
1361
|
+
new IrcLeftLaneComponent(content, width => this.#ircSplitView.effectiveSidebarVisible(width)),
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1306
1365
|
#renderTodoList(): void {
|
|
1307
1366
|
this.todoContainer.clear();
|
|
1308
1367
|
const phases = this.todoPhases.filter(phase => phase.tasks.length > 0);
|
|
@@ -1319,7 +1378,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1319
1378
|
const activePhase = phases[activeIdx];
|
|
1320
1379
|
if (!activePhase) return;
|
|
1321
1380
|
lines.push(
|
|
1322
|
-
`${indent}${theme.fg("accent", `${hook} ${formatPhaseDisplayName(activePhase.name, activeIdx + 1)}`)}`,
|
|
1381
|
+
`${indent}${theme.fg("accent", `${hook} ${formatPhaseDisplayName(this.#sanitizeTodoText(activePhase.name), activeIdx + 1)}`)}`,
|
|
1323
1382
|
);
|
|
1324
1383
|
const visibleTasks = activePhase.tasks.slice(0, 5);
|
|
1325
1384
|
visibleTasks.forEach((todo, index) => {
|
|
@@ -1330,19 +1389,21 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1330
1389
|
const remaining = activePhase.tasks.length - visibleTasks.length;
|
|
1331
1390
|
lines.push(theme.fg("muted", `${indent} ${hook} +${remaining} more`));
|
|
1332
1391
|
}
|
|
1333
|
-
this
|
|
1392
|
+
this.#addTodoContent(lines);
|
|
1334
1393
|
return;
|
|
1335
1394
|
}
|
|
1336
1395
|
|
|
1337
1396
|
phases.forEach((phase, phaseIndex) => {
|
|
1338
|
-
lines.push(
|
|
1397
|
+
lines.push(
|
|
1398
|
+
`${indent}${theme.fg("accent", `${hook} ${formatPhaseDisplayName(this.#sanitizeTodoText(phase.name), phaseIndex + 1)}`)}`,
|
|
1399
|
+
);
|
|
1339
1400
|
phase.tasks.forEach((todo, index) => {
|
|
1340
1401
|
const prefix = `${indent}${index === 0 ? hook : " "} `;
|
|
1341
1402
|
lines.push(this.#formatTodoLine(todo, prefix));
|
|
1342
1403
|
});
|
|
1343
1404
|
});
|
|
1344
1405
|
|
|
1345
|
-
this
|
|
1406
|
+
this.#addTodoContent(lines);
|
|
1346
1407
|
}
|
|
1347
1408
|
|
|
1348
1409
|
async #loadTodoList(): Promise<void> {
|
|
@@ -1377,17 +1438,38 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1377
1438
|
await this.#planModeController.restoreFromSession(sessionContext);
|
|
1378
1439
|
}
|
|
1379
1440
|
|
|
1441
|
+
isStopped(): boolean {
|
|
1442
|
+
return this.#stopped;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
onStop(callback: () => void): () => void {
|
|
1446
|
+
if (this.#stopped) {
|
|
1447
|
+
callback();
|
|
1448
|
+
return () => {};
|
|
1449
|
+
}
|
|
1450
|
+
this.#stopListeners.add(callback);
|
|
1451
|
+
return () => this.#stopListeners.delete(callback);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1380
1454
|
stop(): void {
|
|
1455
|
+
const wasInitialized = this.isInitialized;
|
|
1456
|
+
this.#stopped = true;
|
|
1457
|
+
for (const listener of this.#stopListeners) {
|
|
1458
|
+
try {
|
|
1459
|
+
listener();
|
|
1460
|
+
} catch (error) {
|
|
1461
|
+
logger.warn("Interactive stop listener failed", { error: String(error) });
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
this.#stopListeners.clear();
|
|
1465
|
+
this.#stopLoadingAnimation();
|
|
1466
|
+
this.#suspendedActivityIndicator = undefined;
|
|
1381
1467
|
this.#petProtocolUnsubscribe?.();
|
|
1382
1468
|
this.#petProtocolUnsubscribe = undefined;
|
|
1383
1469
|
this.#petUnavailableWarningDisposer?.();
|
|
1384
1470
|
this.#petUnavailableWarningDisposer = undefined;
|
|
1385
1471
|
this.petWidget?.dispose();
|
|
1386
1472
|
this.petWidget = undefined;
|
|
1387
|
-
if (this.loadingAnimation) {
|
|
1388
|
-
this.loadingAnimation.stop();
|
|
1389
|
-
this.loadingAnimation = undefined;
|
|
1390
|
-
}
|
|
1391
1473
|
this.#welcomeComponent?.dispose();
|
|
1392
1474
|
this.#welcomeComponent = undefined;
|
|
1393
1475
|
if (this.#sttController) {
|
|
@@ -1395,8 +1477,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1395
1477
|
this.#sttController = undefined;
|
|
1396
1478
|
}
|
|
1397
1479
|
this.#goalModeController.cancelContinuation();
|
|
1398
|
-
this.#extensionUiController.
|
|
1399
|
-
this.#extensionUiController.clearHookWidgets();
|
|
1480
|
+
this.#extensionUiController.dispose();
|
|
1400
1481
|
for (const unsubscribe of this.#eventBusUnsubscribers) {
|
|
1401
1482
|
unsubscribe();
|
|
1402
1483
|
}
|
|
@@ -1420,10 +1501,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1420
1501
|
if (this.#subprocessTeardownUnsubscribe) {
|
|
1421
1502
|
this.#subprocessTeardownUnsubscribe();
|
|
1422
1503
|
}
|
|
1423
|
-
if (this.
|
|
1424
|
-
|
|
1425
|
-
this.isInitialized = false;
|
|
1426
|
-
}
|
|
1504
|
+
if (wasInitialized) this.ui.stop();
|
|
1505
|
+
this.isInitialized = false;
|
|
1427
1506
|
}
|
|
1428
1507
|
|
|
1429
1508
|
async shutdown(): Promise<void> {
|
|
@@ -1539,6 +1618,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1539
1618
|
};
|
|
1540
1619
|
|
|
1541
1620
|
void this.refreshSlashCommandState().catch(error => {
|
|
1621
|
+
if (this.#stopped) return;
|
|
1542
1622
|
logger.warn("Failed to refresh slash command state for custom editor", { error: String(error) });
|
|
1543
1623
|
});
|
|
1544
1624
|
|
|
@@ -1562,11 +1642,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1562
1642
|
this.#pendingSubmissionDispose?.();
|
|
1563
1643
|
this.#pendingSubmissionDispose = undefined;
|
|
1564
1644
|
this.#pendingWorkingMessage = undefined;
|
|
1565
|
-
|
|
1566
|
-
this.loadingAnimation.stop();
|
|
1567
|
-
this.loadingAnimation = undefined;
|
|
1568
|
-
this.statusContainer.clear();
|
|
1569
|
-
}
|
|
1645
|
+
this.stopLoadingAnimation();
|
|
1570
1646
|
this.#uiHelpers.showError(message);
|
|
1571
1647
|
}
|
|
1572
1648
|
|
|
@@ -1612,7 +1688,33 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1612
1688
|
return main && dim ? { main, dim } : undefined;
|
|
1613
1689
|
}
|
|
1614
1690
|
|
|
1615
|
-
|
|
1691
|
+
#activeBackgroundTaskCount(): number {
|
|
1692
|
+
return this.session.getAsyncJobSnapshot()?.running.length ?? 0;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
#stopLoadingAnimation(): void {
|
|
1696
|
+
this.loadingAnimation?.stop();
|
|
1697
|
+
this.loadingAnimation = undefined;
|
|
1698
|
+
this.statusContainer.clear();
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
syncActivityIndicator(): void {
|
|
1702
|
+
if (this.#stopped || this.#activityIndicatorSuspensions > 0 || this.autoCompactionLoader || this.retryLoader)
|
|
1703
|
+
return;
|
|
1704
|
+
const foregroundActive = this.#foregroundActivity || this.session.isStreaming;
|
|
1705
|
+
if (!this.isInitialized && !foregroundActive) {
|
|
1706
|
+
this.#stopLoadingAnimation();
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
const message = resolveActivityIndicatorMessage(
|
|
1710
|
+
this.#foregroundActivity || this.session.isStreaming,
|
|
1711
|
+
this.#activeBackgroundTaskCount(),
|
|
1712
|
+
this.#pendingWorkingMessage ?? this.#defaultWorkingMessage,
|
|
1713
|
+
);
|
|
1714
|
+
if (!message) {
|
|
1715
|
+
this.#stopLoadingAnimation();
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1616
1718
|
if (!this.loadingAnimation) {
|
|
1617
1719
|
this.statusContainer.clear();
|
|
1618
1720
|
this.loadingAnimation = new Loader(
|
|
@@ -1621,42 +1723,66 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1621
1723
|
const accent = this.#getWorkingMessageAccent();
|
|
1622
1724
|
return accent ? `${accent.main}${spinner}\x1b[39m` : theme.fg("accent", spinner);
|
|
1623
1725
|
},
|
|
1624
|
-
|
|
1625
|
-
|
|
1726
|
+
workingMessage => renderWorkingMessage(workingMessage, this.#getWorkingMessageAccent()),
|
|
1727
|
+
message,
|
|
1626
1728
|
getSymbolTheme().spinnerFrames,
|
|
1627
1729
|
{ timeDependentColor: true },
|
|
1628
1730
|
);
|
|
1629
1731
|
this.statusContainer.addChild(this.loadingAnimation);
|
|
1630
1732
|
}
|
|
1733
|
+
this.loadingAnimation.setMessage(message);
|
|
1734
|
+
}
|
|
1631
1735
|
|
|
1632
|
-
|
|
1736
|
+
ensureLoadingAnimation(): void {
|
|
1737
|
+
this.#foregroundActivity = true;
|
|
1738
|
+
this.syncActivityIndicator();
|
|
1633
1739
|
}
|
|
1634
1740
|
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
this.loadingAnimation.setMessage(this.#defaultWorkingMessage);
|
|
1640
|
-
}
|
|
1741
|
+
stopLoadingAnimation(options?: { restoreBackground?: boolean }): void {
|
|
1742
|
+
this.#foregroundActivity = false;
|
|
1743
|
+
if (options?.restoreBackground === false) {
|
|
1744
|
+
this.#stopLoadingAnimation();
|
|
1641
1745
|
return;
|
|
1642
1746
|
}
|
|
1747
|
+
this.syncActivityIndicator();
|
|
1748
|
+
}
|
|
1643
1749
|
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1750
|
+
suspendActivityIndicator(): () => void {
|
|
1751
|
+
const isFirstSuspension = this.#activityIndicatorSuspensions++ === 0;
|
|
1752
|
+
if (isFirstSuspension && !this.autoCompactionLoader && !this.retryLoader) {
|
|
1753
|
+
const loadingAnimation = this.loadingAnimation;
|
|
1754
|
+
if (loadingAnimation && this.statusContainer.children.includes(loadingAnimation)) {
|
|
1755
|
+
this.statusContainer.detachChild(loadingAnimation);
|
|
1756
|
+
this.#suspendedActivityIndicator = loadingAnimation;
|
|
1757
|
+
}
|
|
1647
1758
|
}
|
|
1759
|
+
let released = false;
|
|
1760
|
+
return () => {
|
|
1761
|
+
if (released) return;
|
|
1762
|
+
released = true;
|
|
1763
|
+
this.#activityIndicatorSuspensions = Math.max(0, this.#activityIndicatorSuspensions - 1);
|
|
1764
|
+
if (this.#activityIndicatorSuspensions > 0) return;
|
|
1765
|
+
const suspended = this.#suspendedActivityIndicator;
|
|
1766
|
+
this.#suspendedActivityIndicator = undefined;
|
|
1767
|
+
if (
|
|
1768
|
+
!this.#stopped &&
|
|
1769
|
+
suspended &&
|
|
1770
|
+
this.loadingAnimation === suspended &&
|
|
1771
|
+
!this.statusContainer.children.includes(suspended)
|
|
1772
|
+
) {
|
|
1773
|
+
this.statusContainer.addChild(suspended);
|
|
1774
|
+
}
|
|
1775
|
+
this.syncActivityIndicator();
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1648
1778
|
|
|
1779
|
+
setWorkingMessage(message?: string): void {
|
|
1649
1780
|
this.#pendingWorkingMessage = message;
|
|
1781
|
+
if (this.#foregroundActivity) this.syncActivityIndicator();
|
|
1650
1782
|
}
|
|
1651
1783
|
|
|
1652
1784
|
applyPendingWorkingMessage(): void {
|
|
1653
|
-
|
|
1654
|
-
return;
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
const message = this.#pendingWorkingMessage;
|
|
1658
|
-
this.#pendingWorkingMessage = undefined;
|
|
1659
|
-
this.setWorkingMessage(message);
|
|
1785
|
+
this.syncActivityIndicator();
|
|
1660
1786
|
}
|
|
1661
1787
|
|
|
1662
1788
|
showNewVersionNotification(newVersion: string): void {
|
|
@@ -2275,6 +2401,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2275
2401
|
|
|
2276
2402
|
async reloadTodos(): Promise<void> {
|
|
2277
2403
|
await this.#loadTodoList();
|
|
2404
|
+
if (this.#stopped) return;
|
|
2278
2405
|
this.ui.requestRender();
|
|
2279
2406
|
}
|
|
2280
2407
|
|
|
@@ -178,7 +178,7 @@ export class JobsObserver {
|
|
|
178
178
|
|
|
179
179
|
/** Cancel a running monitor job. Returns true when the job was cancelled. */
|
|
180
180
|
cancelMonitor(id: string): boolean {
|
|
181
|
-
return this.#manager.cancel(id);
|
|
181
|
+
return this.#manager.cancel(id, this.#ownerId ? { ownerId: this.#ownerId } : undefined);
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
/** Delete a visible scheduled cron job. Returns true when removed. */
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -1826,32 +1826,34 @@ export async function initTheme(
|
|
|
1826
1826
|
export async function setTheme(
|
|
1827
1827
|
name: string,
|
|
1828
1828
|
enableWatcher: boolean = false,
|
|
1829
|
+
options?: { shouldApply?: () => boolean },
|
|
1829
1830
|
): Promise<{ success: boolean; error?: string }> {
|
|
1830
|
-
autoDetectedTheme = false;
|
|
1831
|
-
previewThemeActive = false;
|
|
1832
|
-
currentThemeName = name;
|
|
1833
1831
|
const requestId = ++themeLoadRequestId;
|
|
1832
|
+
const shouldApply = options?.shouldApply ?? (() => true);
|
|
1834
1833
|
try {
|
|
1835
1834
|
const loadedTheme = await loadTheme(name, getCurrentThemeOptions());
|
|
1836
1835
|
if (requestId !== themeLoadRequestId) {
|
|
1837
1836
|
return { success: false, error: "Theme change superseded by a newer request" };
|
|
1838
1837
|
}
|
|
1838
|
+
if (!shouldApply()) return { success: false, error: "Theme change cancelled" };
|
|
1839
|
+
autoDetectedTheme = false;
|
|
1840
|
+
previewThemeActive = false;
|
|
1841
|
+
currentThemeName = name;
|
|
1839
1842
|
theme = loadedTheme;
|
|
1840
1843
|
if (enableWatcher) {
|
|
1841
1844
|
await startThemeWatcher();
|
|
1842
1845
|
}
|
|
1843
|
-
if (
|
|
1844
|
-
|
|
1845
|
-
}
|
|
1846
|
+
if (!shouldApply()) return { success: false, error: "Theme change cancelled" };
|
|
1847
|
+
onThemeChangeCallback?.();
|
|
1846
1848
|
return { success: true };
|
|
1847
1849
|
} catch (error) {
|
|
1848
1850
|
if (requestId !== themeLoadRequestId) {
|
|
1849
1851
|
return { success: false, error: "Theme change superseded by a newer request" };
|
|
1850
1852
|
}
|
|
1851
|
-
|
|
1853
|
+
const fallbackTheme = await loadTheme("red-claw", getCurrentThemeOptions());
|
|
1854
|
+
if (!shouldApply()) return { success: false, error: "Theme change cancelled" };
|
|
1852
1855
|
currentThemeName = "red-claw";
|
|
1853
|
-
theme =
|
|
1854
|
-
// Don't start watcher for fallback theme
|
|
1856
|
+
theme = fallbackTheme;
|
|
1855
1857
|
return {
|
|
1856
1858
|
success: false,
|
|
1857
1859
|
error: error instanceof Error ? error.message : String(error),
|