@sayknow-cli/coding-agent 0.6.5 → 0.6.7
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 +29 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +79 -0
- package/dist/types/i18n/messages/en.d.ts +6 -29
- package/dist/types/modes/components/welcome.d.ts +34 -40
- package/dist/types/modes/interactive-mode.d.ts +7 -4
- package/dist/types/modes/types.d.ts +4 -0
- package/dist/types/sdk/broker/broker.d.ts +22 -0
- package/dist/types/sdk/broker/process-guard.d.ts +71 -0
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/session/auth-storage-discovery.d.ts +15 -0
- package/dist/types/session/auto-fallback.d.ts +27 -0
- package/dist/types/session/fallback-chain-controller.d.ts +5 -0
- package/dist/types/session/response-language.d.ts +25 -0
- package/dist/types/setup/model-onboarding-guidance.d.ts +8 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/tools/debug.d.ts +2 -2
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/locate-core.d.ts +97 -0
- package/dist/types/tools/locate.d.ts +38 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/src/cli/setup-cli.ts +7 -4
- package/src/commands/sdk.ts +3 -0
- package/src/commands/setup.ts +4 -1
- package/src/config/keybindings.ts +7 -0
- package/src/config/settings-schema.ts +82 -0
- package/src/decisions/typesafe-backend.ts +38 -4
- package/src/i18n/messages/de.settings.ts +26 -0
- package/src/i18n/messages/de.ts +4 -27
- package/src/i18n/messages/en.ts +6 -29
- package/src/i18n/messages/es.settings.ts +26 -0
- package/src/i18n/messages/es.ts +4 -27
- package/src/i18n/messages/fr.settings.ts +26 -0
- package/src/i18n/messages/fr.ts +4 -27
- package/src/i18n/messages/ja.settings.ts +26 -0
- package/src/i18n/messages/ja.ts +4 -27
- package/src/i18n/messages/ko.settings.ts +25 -0
- package/src/i18n/messages/ko.ts +5 -28
- package/src/i18n/messages/zh.settings.ts +23 -0
- package/src/i18n/messages/zh.ts +4 -27
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/modes/action-registry.ts +1 -0
- package/src/modes/components/welcome.ts +385 -387
- package/src/modes/controllers/input-controller.ts +15 -0
- package/src/modes/controllers/selector-controller.ts +13 -0
- package/src/modes/interactive-mode.ts +160 -98
- package/src/modes/types.ts +4 -0
- package/src/prompts/system/system-prompt.md +7 -2
- package/src/prompts/tools/locate.md +12 -0
- package/src/sdk/broker/broker.ts +75 -1
- package/src/sdk/broker/process-guard.ts +160 -0
- package/src/sdk/broker/transport.ts +15 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +13 -1
- package/src/sdk/protocol/operation-inventory.generated.json +22 -0
- package/src/sdk/session.ts +4 -1
- package/src/session/agent-session.ts +118 -2
- package/src/session/auth-storage-discovery.ts +22 -7
- package/src/session/auto-fallback.ts +59 -0
- package/src/session/fallback-chain-controller.ts +5 -0
- package/src/session/response-language.ts +71 -0
- package/src/setup/model-onboarding-guidance.ts +29 -14
- package/src/setup/provider-onboarding.ts +5 -0
- package/src/slash-commands/builtin-registry.ts +112 -2
- package/src/tools/index.ts +3 -0
- package/src/tools/locate-core.ts +720 -0
- package/src/tools/locate.ts +197 -0
|
@@ -112,6 +112,9 @@ export class InputController {
|
|
|
112
112
|
"app.session.tree": () => this.ctx.showTreeSelector(),
|
|
113
113
|
"app.session.fork": () => this.ctx.showUserMessageSelector(),
|
|
114
114
|
"app.session.resume": () => this.ctx.showSessionSelector(),
|
|
115
|
+
"app.session.continue": async () => {
|
|
116
|
+
await this.ctx.continueRecentSession();
|
|
117
|
+
},
|
|
115
118
|
"app.session.observe": async () => {
|
|
116
119
|
await this.ctx.showSessionObserver();
|
|
117
120
|
},
|
|
@@ -710,6 +713,16 @@ export class InputController {
|
|
|
710
713
|
return true;
|
|
711
714
|
});
|
|
712
715
|
}
|
|
716
|
+
this.#registerCommandPaletteAction("app.session.continue", () => {
|
|
717
|
+
void this.ctx.continueRecentSession();
|
|
718
|
+
return undefined;
|
|
719
|
+
});
|
|
720
|
+
for (const key of this.ctx.keybindings.getKeys("app.session.continue")) {
|
|
721
|
+
this.ctx.editor.setCustomKeyHandler(key, () => {
|
|
722
|
+
this.#executeAction("app.session.continue");
|
|
723
|
+
return true;
|
|
724
|
+
});
|
|
725
|
+
}
|
|
713
726
|
this.#registerCommandPaletteAction(
|
|
714
727
|
"app.message.followUp",
|
|
715
728
|
() => this.#executeAction("app.message.followUp"),
|
|
@@ -835,6 +848,8 @@ export class InputController {
|
|
|
835
848
|
|
|
836
849
|
async submitText(text: string, composer: ComposerSubmissionOptions): Promise<void> {
|
|
837
850
|
text = text.trim();
|
|
851
|
+
// Past the launch card: give the mouse back to the terminal and stop offering its rows.
|
|
852
|
+
if (text) this.ctx.endWelcomeInteraction?.();
|
|
838
853
|
if ((!isSettingsInitialized() || settings.get("emojiAutocomplete")) && text) text = expandEmoticons(text);
|
|
839
854
|
if (this.ctx.hasActiveBtw()) {
|
|
840
855
|
if (!text) return;
|
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
} from "../../sdk/bus/telegram-daemon";
|
|
71
71
|
import { TelegramDaemonController } from "../../sdk/bus/telegram-daemon-control";
|
|
72
72
|
import { runTelegramSetup, type TelegramSetupPreflight } from "../../sdk/bus/telegram-setup";
|
|
73
|
+
import { applyCredentialRankingModeSetting } from "../../session/auth-storage-discovery";
|
|
73
74
|
import { type SessionInfo, SessionManager } from "../../session/session-manager";
|
|
74
75
|
import { getTreeForInternalRead } from "../../session/session-manager-internal";
|
|
75
76
|
|
|
@@ -1372,6 +1373,18 @@ export class SelectorController {
|
|
|
1372
1373
|
});
|
|
1373
1374
|
break;
|
|
1374
1375
|
}
|
|
1376
|
+
case "auth.credentialRankingMode": {
|
|
1377
|
+
const effective = applyCredentialRankingModeSetting(
|
|
1378
|
+
this.ctx.session.modelRegistry.authStorage,
|
|
1379
|
+
this.ctx.settings,
|
|
1380
|
+
);
|
|
1381
|
+
if (effective !== value) {
|
|
1382
|
+
this.ctx.showWarning(
|
|
1383
|
+
`SKC_CREDENTIAL_RANKING_MODE=${effective} is set, so it overrides this setting on this machine.`,
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
break;
|
|
1387
|
+
}
|
|
1375
1388
|
case "clearOnShrink":
|
|
1376
1389
|
this.ctx.ui.setClearOnShrink(value as boolean);
|
|
1377
1390
|
break;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import { type Agent, type AgentMessage, ThinkingLevel } from "@sayknow-cli/agent-core";
|
|
3
4
|
import type { CompactionOutcome } from "@sayknow-cli/agent-core/compaction";
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
clearRenderCache,
|
|
9
10
|
getRenderCacheRetainedBytes,
|
|
10
11
|
Loader,
|
|
12
|
+
matchesKey,
|
|
11
13
|
onImageProtocolChanged,
|
|
12
14
|
ProcessTerminal,
|
|
13
15
|
Spacer,
|
|
@@ -25,7 +27,6 @@ import {
|
|
|
25
27
|
KeybindingsManager,
|
|
26
28
|
type KeyDisplayContext,
|
|
27
29
|
} from "../config/keybindings";
|
|
28
|
-
import { getModelProfilePresentation } from "../config/model-profiles";
|
|
29
30
|
import { isSettingsInitialized, type Settings, settings } from "../config/settings";
|
|
30
31
|
import { DEFAULT_SKC_DEFINITION_NAMES } from "../defaults/skc-defaults";
|
|
31
32
|
import type {
|
|
@@ -48,7 +49,6 @@ import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
|
48
49
|
import { HistoryStorage } from "../session/history-storage";
|
|
49
50
|
import type { SessionContext, SessionManager } from "../session/session-manager";
|
|
50
51
|
import { getRecentSessions, getSessionMessageEntryId } from "../session/session-manager";
|
|
51
|
-
import { loadProjectContextFiles } from "../system-prompt";
|
|
52
52
|
import type { LspStartupServerInfo } from "../tools";
|
|
53
53
|
import { shortenPath } from "../tools/render-utils";
|
|
54
54
|
import { formatPhaseDisplayName } from "../tools/todo-write";
|
|
@@ -72,19 +72,12 @@ import {
|
|
|
72
72
|
isPetCapabilityProbePending,
|
|
73
73
|
warnWhenPetCapabilitySettled,
|
|
74
74
|
} from "./components/pet-capability";
|
|
75
|
-
import { type PetMode, SayknowPetWidget } from "./components/sayknow-pet-widget";
|
|
75
|
+
import { type PetMode, type PetSkinId, SayknowPetWidget } from "./components/sayknow-pet-widget";
|
|
76
76
|
import { resolveCurrentBranch } from "./components/status-line/git-utils";
|
|
77
77
|
import type { ToolExecutionHandle } from "./components/tool-execution";
|
|
78
78
|
import { StatusLineComponent } from "./components/tool-status-header";
|
|
79
79
|
import { composeToolText } from "./components/tool-transcript-format";
|
|
80
|
-
import {
|
|
81
|
-
type RecentSession,
|
|
82
|
-
WelcomeComponent,
|
|
83
|
-
type WelcomeLogoMode,
|
|
84
|
-
type LspServerInfo as WelcomeLspServerInfo,
|
|
85
|
-
type WelcomeRoleBinding,
|
|
86
|
-
type WelcomeSnapshot,
|
|
87
|
-
} from "./components/welcome";
|
|
80
|
+
import { type RecentSession, WelcomeComponent, type WelcomeLogoMode, type WelcomeSnapshot } from "./components/welcome";
|
|
88
81
|
import { BtwController } from "./controllers/btw-controller";
|
|
89
82
|
import { CommandController } from "./controllers/command-controller";
|
|
90
83
|
import { EventController } from "./controllers/event-controller";
|
|
@@ -107,7 +100,14 @@ import { shouldShowExtensionCommand } from "./slash-command-visibility";
|
|
|
107
100
|
import { TasksAggregator } from "./tasks-aggregator";
|
|
108
101
|
import { type ShimmerPalette, shimmerSegments, shimmerText } from "./theme/shimmer";
|
|
109
102
|
import type { Theme } from "./theme/theme";
|
|
110
|
-
import {
|
|
103
|
+
import {
|
|
104
|
+
getCurrentThemeName,
|
|
105
|
+
getEditorTheme,
|
|
106
|
+
getSymbolTheme,
|
|
107
|
+
onTerminalAppearanceChange,
|
|
108
|
+
onThemeChange,
|
|
109
|
+
theme,
|
|
110
|
+
} from "./theme/theme";
|
|
111
111
|
import { type RegisterTranscriptItem, TranscriptItemRegistry, transcriptItemId } from "./transcript-item-registry";
|
|
112
112
|
import {
|
|
113
113
|
type CompactionQueuedMessage,
|
|
@@ -211,13 +211,41 @@ function getShellInputPrefix(isNoContext: boolean): string {
|
|
|
211
211
|
return `${shellLabel} `;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
/**
|
|
215
|
+
* The pet on the launch card: the one the user keeps beside the composer, or —
|
|
216
|
+
* with the pet off — the skin that matches the theme (blue for blue-octopus).
|
|
217
|
+
*/
|
|
218
|
+
export function resolveWelcomePetSkin(petMode: PetMode, themeName: string | undefined): PetSkinId {
|
|
219
|
+
if (petMode !== "off") return petMode;
|
|
220
|
+
return themeName?.startsWith("blue") ? "blue" : "red";
|
|
221
|
+
}
|
|
222
|
+
|
|
214
223
|
/**
|
|
215
224
|
* The composer is an open rail, not a box: every input row starts with the same
|
|
216
225
|
* rail glyph a submitted prompt keeps in the transcript, colored by the editor's
|
|
217
226
|
* border color (session accent, thinking level, shell/python mode).
|
|
218
227
|
*/
|
|
228
|
+
/** Background band under the composer's input rows, so the line being typed stands out. */
|
|
229
|
+
function composerInputBackground(): string | undefined {
|
|
230
|
+
try {
|
|
231
|
+
return theme.getBgAnsi("userMessageBg");
|
|
232
|
+
} catch {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Canonical form of a session path, so /private/var and /var compare equal on macOS. */
|
|
238
|
+
function canonicalPath(file: string): string {
|
|
239
|
+
try {
|
|
240
|
+
return fs.realpathSync(file);
|
|
241
|
+
} catch {
|
|
242
|
+
return path.resolve(file);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
219
246
|
export function configureDefaultComposerChrome(editor: CustomEditor): void {
|
|
220
247
|
editor.setBorderVisible(false);
|
|
248
|
+
editor.setInputBackground(composerInputBackground());
|
|
221
249
|
editor.setClosedBorderBox(false);
|
|
222
250
|
editor.setPromptGutter(undefined);
|
|
223
251
|
editor.setRailGutter(theme.rail.user);
|
|
@@ -464,6 +492,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
464
492
|
#eventBus?: EventBus;
|
|
465
493
|
#eventBusUnsubscribers: Array<() => void> = [];
|
|
466
494
|
#welcomeComponent?: WelcomeComponent;
|
|
495
|
+
#welcomeInputUnsubscribe?: () => void;
|
|
496
|
+
/** Mouse reporting was switched on for the launch card and must be restored after it. */
|
|
497
|
+
#welcomeEnabledMouse = false;
|
|
467
498
|
#ircSplitView: IrcSplitViewComponent;
|
|
468
499
|
#ircSidebarAvailable = false;
|
|
469
500
|
#ircSidebarRequestedVisible = false;
|
|
@@ -743,7 +774,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
743
774
|
modelName,
|
|
744
775
|
providerName,
|
|
745
776
|
recentSessions,
|
|
746
|
-
this.#getWelcomeLspServers(),
|
|
747
777
|
welcomeLogoMode,
|
|
748
778
|
{
|
|
749
779
|
getViewportRows: () => this.ui.terminal.rows,
|
|
@@ -755,11 +785,15 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
755
785
|
skipLogoAnimation,
|
|
756
786
|
snapshot: this.#buildWelcomeSnapshot(),
|
|
757
787
|
resumeKey: this.keybindings.getKeys("app.session.resume")[0],
|
|
788
|
+
continueKey: this.keybindings.getKeys("app.session.continue")[0],
|
|
789
|
+
petSkin: resolveWelcomePetSkin(settings.get("pet.mode"), getCurrentThemeName()),
|
|
790
|
+
onOpenSession: session => void this.#openWelcomeSession(session),
|
|
758
791
|
},
|
|
759
792
|
);
|
|
760
793
|
|
|
761
794
|
this.ui.addChild(this.#welcomeComponent);
|
|
762
795
|
this.#welcomeComponent.playIntro(() => this.ui.requestRender());
|
|
796
|
+
this.#startWelcomeInteraction();
|
|
763
797
|
}
|
|
764
798
|
|
|
765
799
|
this.ui.addChild(this.#ircSplitView);
|
|
@@ -871,11 +905,15 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
871
905
|
)
|
|
872
906
|
.then(sessions => {
|
|
873
907
|
if (this.#welcomeComponent !== welcomeComponent) return;
|
|
908
|
+
const current = this.#currentSessionFileCanonical();
|
|
874
909
|
welcomeComponent.setRecentSessions(
|
|
875
|
-
sessions
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
910
|
+
sessions
|
|
911
|
+
.filter(session => canonicalPath(session.path) !== current)
|
|
912
|
+
.map(session => ({
|
|
913
|
+
name: session.name,
|
|
914
|
+
timeAgo: session.timeAgo,
|
|
915
|
+
path: session.path,
|
|
916
|
+
})),
|
|
879
917
|
);
|
|
880
918
|
this.ui.requestRender();
|
|
881
919
|
})
|
|
@@ -1222,6 +1260,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1222
1260
|
}
|
|
1223
1261
|
// Re-read the glyph so a symbol-preset switch (unicode/nerd/ascii) reaches the rail.
|
|
1224
1262
|
this.editor.setRailGutter(theme.rail.user);
|
|
1263
|
+
this.editor.setInputBackground(composerInputBackground());
|
|
1225
1264
|
this.editor.setPlaceholder(this.#getComposerPlaceholder());
|
|
1226
1265
|
this.#setComposerTopBorder();
|
|
1227
1266
|
this.ui.requestRender();
|
|
@@ -1424,6 +1463,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1424
1463
|
this.loadingAnimation.stop();
|
|
1425
1464
|
this.loadingAnimation = undefined;
|
|
1426
1465
|
}
|
|
1466
|
+
this.#endWelcomeInteraction();
|
|
1427
1467
|
this.#welcomeComponent?.dispose();
|
|
1428
1468
|
this.#welcomeComponent = undefined;
|
|
1429
1469
|
if (this.#sttController) {
|
|
@@ -1611,36 +1651,13 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1611
1651
|
}
|
|
1612
1652
|
|
|
1613
1653
|
#handleLspStartupEvent(event: LspStartupEvent): void {
|
|
1614
|
-
this.#updateWelcomeLspServers();
|
|
1615
|
-
|
|
1616
1654
|
const warningMessage = getLspStartupWarningMessage(event);
|
|
1617
1655
|
if (warningMessage) {
|
|
1618
1656
|
this.showWarning(warningMessage);
|
|
1619
1657
|
}
|
|
1620
1658
|
}
|
|
1621
1659
|
|
|
1622
|
-
|
|
1623
|
-
return (
|
|
1624
|
-
this.lspServers?.map(server => ({
|
|
1625
|
-
name: server.name,
|
|
1626
|
-
status: server.status,
|
|
1627
|
-
fileTypes: server.fileTypes,
|
|
1628
|
-
})) ?? []
|
|
1629
|
-
);
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
#updateWelcomeLspServers(): void {
|
|
1633
|
-
if (!this.#welcomeComponent) {
|
|
1634
|
-
return;
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
this.#welcomeComponent.setLspServers(this.#getWelcomeLspServers());
|
|
1638
|
-
// LSP startup settles around the same time the MCP connections do.
|
|
1639
|
-
this.#welcomeComponent.setSnapshot({ mcp: this.#welcomeMcpSummary() });
|
|
1640
|
-
this.ui.requestRender();
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
/** Facts the launch ledger can show synchronously, before any probe runs. */
|
|
1660
|
+
/** Facts the launch card can show synchronously, before the git probe runs. */
|
|
1644
1661
|
#buildWelcomeSnapshot(): WelcomeSnapshot {
|
|
1645
1662
|
const projectDir = getProjectDir();
|
|
1646
1663
|
let branch: string | null = null;
|
|
@@ -1649,68 +1666,19 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1649
1666
|
} catch {
|
|
1650
1667
|
branch = null;
|
|
1651
1668
|
}
|
|
1652
|
-
|
|
1653
|
-
return {
|
|
1654
|
-
cwd: shortenPath(projectDir),
|
|
1655
|
-
branch,
|
|
1656
|
-
thinkingLevel: this.session.thinkingLevel,
|
|
1657
|
-
profile: profileName ? getModelProfilePresentation(profileName).displayName : undefined,
|
|
1658
|
-
roles: this.#welcomeRoleBindings(),
|
|
1659
|
-
mcp: this.#welcomeMcpSummary(),
|
|
1660
|
-
skills: this.session.skills.length,
|
|
1661
|
-
};
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
/** Role agents in their canonical order, with the provider prefix dropped for width. */
|
|
1665
|
-
#welcomeRoleBindings(): WelcomeRoleBinding[] {
|
|
1666
|
-
const overrides = settings.get("task.agentModelOverrides") as Record<string, unknown> | undefined;
|
|
1667
|
-
if (!overrides) return [];
|
|
1668
|
-
const bindings: WelcomeRoleBinding[] = [];
|
|
1669
|
-
for (const role of ["executor", "planner", "critic", "architect"]) {
|
|
1670
|
-
const selector = overrides[role];
|
|
1671
|
-
if (typeof selector !== "string" || selector.length === 0) continue;
|
|
1672
|
-
const slash = selector.indexOf("/");
|
|
1673
|
-
bindings.push({ role, model: slash >= 0 ? selector.slice(slash + 1) : selector });
|
|
1674
|
-
}
|
|
1675
|
-
return bindings;
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
#welcomeMcpSummary(): { connected: number; total: number } {
|
|
1679
|
-
const manager = this.mcpManager;
|
|
1680
|
-
if (!manager) return { connected: 0, total: 0 };
|
|
1681
|
-
return { connected: manager.getConnectedServers().length, total: manager.getAllServerNames().length };
|
|
1669
|
+
return { cwd: shortenPath(projectDir), branch, thinkingLevel: this.session.thinkingLevel };
|
|
1682
1670
|
}
|
|
1683
1671
|
|
|
1684
|
-
/** Fill in the
|
|
1672
|
+
/** Fill in the git change counts, which need I/O. */
|
|
1685
1673
|
#probeWelcomeWorkspace(welcomeComponent: WelcomeComponent): void {
|
|
1686
|
-
const projectDir = getProjectDir();
|
|
1687
|
-
const apply = (patch: WelcomeSnapshot): void => {
|
|
1688
|
-
if (this.#welcomeComponent !== welcomeComponent) return;
|
|
1689
|
-
welcomeComponent.setSnapshot(patch);
|
|
1690
|
-
this.ui.requestRender();
|
|
1691
|
-
};
|
|
1692
1674
|
void gitUtils.status
|
|
1693
|
-
.summary(
|
|
1694
|
-
.then(gitChanges =>
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1675
|
+
.summary(getProjectDir())
|
|
1676
|
+
.then(gitChanges => {
|
|
1677
|
+
if (this.#welcomeComponent !== welcomeComponent) return;
|
|
1678
|
+
welcomeComponent.setSnapshot({ gitChanges });
|
|
1679
|
+
this.ui.requestRender();
|
|
1680
|
+
})
|
|
1699
1681
|
.catch(() => {});
|
|
1700
|
-
void loadProjectContextFiles({ cwd: projectDir })
|
|
1701
|
-
.then(files =>
|
|
1702
|
-
apply({
|
|
1703
|
-
contextFiles: files.map(file => {
|
|
1704
|
-
const relative = path.relative(projectDir, file.path);
|
|
1705
|
-
return relative.startsWith("..") ? shortenPath(file.path) : relative;
|
|
1706
|
-
}),
|
|
1707
|
-
}),
|
|
1708
|
-
)
|
|
1709
|
-
.catch(error => {
|
|
1710
|
-
logger.debug("Failed to load context files for welcome screen", {
|
|
1711
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1712
|
-
});
|
|
1713
|
-
});
|
|
1714
1682
|
}
|
|
1715
1683
|
|
|
1716
1684
|
#getWorkingMessageAccent(): WorkingMessageAccent | undefined {
|
|
@@ -2246,6 +2214,100 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2246
2214
|
this.#selectorController.showSessionSelector();
|
|
2247
2215
|
}
|
|
2248
2216
|
|
|
2217
|
+
#currentSessionFileCanonical(): string | undefined {
|
|
2218
|
+
const currentFile = this.sessionManager.getSessionFile();
|
|
2219
|
+
return currentFile ? canonicalPath(currentFile) : undefined;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
/**
|
|
2223
|
+
* The launch card's session rows take ↓/↑/Enter/Esc and clicks until the first prompt
|
|
2224
|
+
* is sent. Mouse reporting is switched on for that window (unless the user already
|
|
2225
|
+
* runs with it) so a click reaches the card; it goes back to the configured state when
|
|
2226
|
+
* the card stops taking input.
|
|
2227
|
+
*/
|
|
2228
|
+
#startWelcomeInteraction(): void {
|
|
2229
|
+
this.#releaseWelcomeInput();
|
|
2230
|
+
const welcome = this.#welcomeComponent;
|
|
2231
|
+
if (!welcome) return;
|
|
2232
|
+
this.#welcomeInputUnsubscribe = this.ui.addInputListener(data => this.#handleWelcomeKey(welcome, data));
|
|
2233
|
+
if (settings.get("startup.welcomeMouse") && !this.ui.mouseEnabled) {
|
|
2234
|
+
this.ui.setMouseEnabled(true);
|
|
2235
|
+
this.#welcomeEnabledMouse = true;
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
/** The card stops taking input: first prompt sent, a session opened, or the UI rebuilt. */
|
|
2240
|
+
endWelcomeInteraction(): void {
|
|
2241
|
+
this.#endWelcomeInteraction();
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
/** Drop the card's key listener and give mouse reporting back to its configured state. */
|
|
2245
|
+
#releaseWelcomeInput(): void {
|
|
2246
|
+
this.#welcomeInputUnsubscribe?.();
|
|
2247
|
+
this.#welcomeInputUnsubscribe = undefined;
|
|
2248
|
+
if (this.#welcomeEnabledMouse) {
|
|
2249
|
+
this.#welcomeEnabledMouse = false;
|
|
2250
|
+
this.ui.setMouseEnabled(settings.get("mouse.enabled"));
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
#endWelcomeInteraction(): void {
|
|
2255
|
+
this.#releaseWelcomeInput();
|
|
2256
|
+
if (this.#welcomeComponent?.interactive) {
|
|
2257
|
+
this.#welcomeComponent.endInteraction();
|
|
2258
|
+
this.ui.requestRender();
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
/**
|
|
2263
|
+
* ↓ from an empty composer (not walking history) moves into the session rows; there,
|
|
2264
|
+
* ↑/↓ move, Enter opens, Esc (or ↑ past the first row) hands focus back. Any other key
|
|
2265
|
+
* leaves the rows and reaches the composer as usual.
|
|
2266
|
+
*/
|
|
2267
|
+
#handleWelcomeKey(welcome: WelcomeComponent, data: string): { consume: true } | undefined {
|
|
2268
|
+
if (this.#welcomeComponent !== welcome || !welcome.interactive) return undefined;
|
|
2269
|
+
if (this.ui.getFocusedComponent() !== this.editor) return undefined;
|
|
2270
|
+
const picking = welcome.selectedIndex !== undefined;
|
|
2271
|
+
if (!picking) {
|
|
2272
|
+
if (!matchesKey(data, "down")) return undefined;
|
|
2273
|
+
if (this.editor.getText() !== "" || this.editor.isBrowsingHistory() || this.editor.isShowingAutocomplete())
|
|
2274
|
+
return undefined;
|
|
2275
|
+
if (!welcome.select(0)) return undefined;
|
|
2276
|
+
this.ui.requestRender();
|
|
2277
|
+
return { consume: true };
|
|
2278
|
+
}
|
|
2279
|
+
if (matchesKey(data, "down")) welcome.moveSelection(1);
|
|
2280
|
+
else if (matchesKey(data, "up")) welcome.moveSelection(-1);
|
|
2281
|
+
else if (matchesKey(data, "enter")) welcome.openSelected();
|
|
2282
|
+
else if (matchesKey(data, "escape")) welcome.select(undefined);
|
|
2283
|
+
else {
|
|
2284
|
+
welcome.select(undefined);
|
|
2285
|
+
this.ui.requestRender();
|
|
2286
|
+
return undefined;
|
|
2287
|
+
}
|
|
2288
|
+
this.ui.requestRender();
|
|
2289
|
+
return { consume: true };
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
async #openWelcomeSession(session: RecentSession): Promise<void> {
|
|
2293
|
+
if (!session.path) return;
|
|
2294
|
+
this.#endWelcomeInteraction();
|
|
2295
|
+
await this.handleResumeSession(session.path, { requireIdle: true });
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
async continueRecentSession(): Promise<void> {
|
|
2299
|
+
// Listing returns canonical paths (/private/var on macOS); compare canonically so the
|
|
2300
|
+
// live session is never "continued" into itself.
|
|
2301
|
+
const current = this.#currentSessionFileCanonical();
|
|
2302
|
+
const recent = await getRecentSessions(this.sessionManager.getSessionDir());
|
|
2303
|
+
const target = recent.find(session => canonicalPath(session.path) !== current);
|
|
2304
|
+
if (!target) {
|
|
2305
|
+
this.showStatus("No earlier session to continue");
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
await this.handleResumeSession(target.path, { requireIdle: true });
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2249
2311
|
handleResumeSession(sessionPath: string, options?: { requireIdle?: boolean }): Promise<boolean> {
|
|
2250
2312
|
return this.#selectorController.handleResumeSession(sessionPath, options, () => {
|
|
2251
2313
|
this.#btwController.dispose();
|
package/src/modes/types.ts
CHANGED
|
@@ -327,6 +327,10 @@ export interface InteractiveModeContext {
|
|
|
327
327
|
showUserMessageSelector(): void;
|
|
328
328
|
showTreeSelector(): void;
|
|
329
329
|
showSessionSelector(): void;
|
|
330
|
+
/** Resume the most recent saved session other than this one (the launch card's first row). */
|
|
331
|
+
continueRecentSession(): Promise<void>;
|
|
332
|
+
/** The launch card stops taking ↓/Enter/clicks and releases mouse capture (first prompt sent). */
|
|
333
|
+
endWelcomeInteraction?(): void;
|
|
330
334
|
showSessionsDashboard(): void;
|
|
331
335
|
handleResumeSession(sessionPath: string, options?: { requireIdle?: boolean }): Promise<boolean>;
|
|
332
336
|
handleSessionDeleteCommand(): Promise<void>;
|
|
@@ -43,11 +43,16 @@ Optimize for correctness first, maintainability second, and brevity third. Prefe
|
|
|
43
43
|
- Do not defer actionable work. Underpromise and overdeliver: report only what is done or in progress, never announce remaining work instead of doing it.
|
|
44
44
|
</communication>
|
|
45
45
|
|
|
46
|
+
<response-language>
|
|
47
|
+
- Write every user-facing message in the language the user writes in (their latest message); a language they explicitly asked for wins.
|
|
48
|
+
- This holds for the whole turn, including progress notes and the final report after long tool work. Tool output, logs, code, file contents, English skill or system text, and context summaries in another language never change it.
|
|
49
|
+
- Keep code, commands, file paths, identifiers, and quoted output as they are; translate the prose around them.
|
|
50
|
+
</response-language>
|
|
51
|
+
|
|
46
52
|
{{#if reasoningLanguageEnglish}}
|
|
47
53
|
<reasoning-language>
|
|
48
54
|
- Reason through development and technical problem-solving in English.
|
|
49
|
-
-
|
|
50
|
-
- This changes reasoning language only; it does not relax correctness, safety, or communication requirements.
|
|
55
|
+
- This changes reasoning language only; user-facing answers still follow <response-language>, and it does not relax correctness, safety, or communication requirements.
|
|
51
56
|
</reasoning-language>
|
|
52
57
|
{{/if}}
|
|
53
58
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Find where behaviour lives by describing what the code does, when you do not know a file, symbol, or string to grep for.
|
|
2
|
+
|
|
3
|
+
<instruction>
|
|
4
|
+
- Ask a question about behaviour: "Where does the broker stop when idle?", "Which code picks fallback models?", "Which tests cover retry after a timeout?"
|
|
5
|
+
- Returns ranked files with their declaration lines and line numbers. Read the listed lines next; the result is a lead, not proof, and it can miss files.
|
|
6
|
+
- Use `search` or `find` instead when you already know an exact name, path, or string. They are faster and free.
|
|
7
|
+
- Narrow `path` to the part of the repository you care about when you can.
|
|
8
|
+
</instruction>
|
|
9
|
+
|
|
10
|
+
<data>
|
|
11
|
+
Relevance is judged by Jev (TypeSafe). It receives the question, folder paths, file names, and each candidate file's declaration lines (function, class, type, and constant signatures). It never receives function bodies or comments. Files ignored by .gitignore, hidden files, and binaries are skipped.
|
|
12
|
+
</data>
|
package/src/sdk/broker/broker.ts
CHANGED
|
@@ -40,6 +40,18 @@ export interface BrokerSettings {
|
|
|
40
40
|
packageGeneration?: string;
|
|
41
41
|
port?: number;
|
|
42
42
|
heartbeatTtlMs?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Stop after this long with no connection, no request in flight and no live
|
|
45
|
+
* session. Clients call `ensureBroker` before use, so the next one starts a fresh
|
|
46
|
+
* broker. 0 disables. Default {@link BROKER_IDLE_SHUTDOWN_MS}.
|
|
47
|
+
*/
|
|
48
|
+
idleShutdownMs?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Runs at the start of every publication tick, before the tick calls into the
|
|
51
|
+
* native addon. The broker process uses it to stop at once when its source
|
|
52
|
+
* checkout has vanished (see `process-guard.ts`); return true to skip the tick.
|
|
53
|
+
*/
|
|
54
|
+
beforePublicationTick?: () => boolean;
|
|
43
55
|
/** Broker-owned migration policy. Client lifecycle frames cannot select it. */
|
|
44
56
|
resolveDirectoryMigration?: (_cwd: string) => Promise<DirectoryMigrationPolicy>;
|
|
45
57
|
}
|
|
@@ -49,6 +61,8 @@ type ResolvedBrokerSettings = {
|
|
|
49
61
|
packageGeneration: string;
|
|
50
62
|
port: number;
|
|
51
63
|
heartbeatTtlMs: number;
|
|
64
|
+
idleShutdownMs: number;
|
|
65
|
+
beforePublicationTick: () => boolean;
|
|
52
66
|
resolveDirectoryMigration: (_cwd: string) => Promise<DirectoryMigrationPolicy>;
|
|
53
67
|
};
|
|
54
68
|
|
|
@@ -371,6 +385,12 @@ type BrokerLockSnapshot = {
|
|
|
371
385
|
const BROKER_PUBLICATION_CADENCE_MS = 5_000;
|
|
372
386
|
const BROKER_PUBLICATION_GRACE_MS = 15_000;
|
|
373
387
|
const BROKER_SETTLEMENT_MS = 2_000;
|
|
388
|
+
/**
|
|
389
|
+
* A broker with nothing to serve stops after this long. Without it a broker lived
|
|
390
|
+
* until killed: one started by a test (or a crashed client) whose caller never came
|
|
391
|
+
* back stayed up for days holding a port, memory and a mapped native addon.
|
|
392
|
+
*/
|
|
393
|
+
export const BROKER_IDLE_SHUTDOWN_MS = 30 * 60_000;
|
|
374
394
|
type BrokerPublicationState =
|
|
375
395
|
| "healthy-owned"
|
|
376
396
|
| "suspect-unpublished"
|
|
@@ -400,12 +420,17 @@ export class Broker {
|
|
|
400
420
|
#completion!: Promise<void>;
|
|
401
421
|
#resolveCompletion!: () => void;
|
|
402
422
|
#rejectCompletion!: (error: unknown) => void;
|
|
423
|
+
/** Last time a client connected, disconnected, or sent a request. Drives idle shutdown. */
|
|
424
|
+
#lastActivityAt = Date.now();
|
|
425
|
+
#idleCheckInFlight = false;
|
|
403
426
|
constructor(settings: BrokerSettings) {
|
|
404
427
|
this.settings = {
|
|
405
428
|
agentDir: settings.agentDir,
|
|
406
429
|
packageGeneration: settings.packageGeneration ?? "unknown",
|
|
407
430
|
port: settings.port ?? 0,
|
|
408
431
|
heartbeatTtlMs: settings.heartbeatTtlMs ?? BROKER_HEARTBEAT_TTL_MS,
|
|
432
|
+
idleShutdownMs: Math.max(0, settings.idleShutdownMs ?? BROKER_IDLE_SHUTDOWN_MS),
|
|
433
|
+
beforePublicationTick: settings.beforePublicationTick ?? (() => false),
|
|
409
434
|
resolveDirectoryMigration: settings.resolveDirectoryMigration ?? (async () => "copy-retain"),
|
|
410
435
|
};
|
|
411
436
|
this.index = new SessionIndex(settings.agentDir);
|
|
@@ -598,7 +623,8 @@ export class Broker {
|
|
|
598
623
|
10,
|
|
599
624
|
Math.min(BROKER_PUBLICATION_CADENCE_MS, Math.floor(this.settings.heartbeatTtlMs / 3)),
|
|
600
625
|
);
|
|
601
|
-
this.#
|
|
626
|
+
this.#lastActivityAt = Date.now();
|
|
627
|
+
this.#heartbeatTimer = setInterval(() => void this.#publicationTick(), cadenceMs);
|
|
602
628
|
return this.discovery;
|
|
603
629
|
} catch (error) {
|
|
604
630
|
await this.#transport?.stop();
|
|
@@ -625,6 +651,52 @@ export class Broker {
|
|
|
625
651
|
if (kind === "suspect-unpublished") this.#lossAt ??= process.hrtime.bigint();
|
|
626
652
|
else this.#lossAt = null;
|
|
627
653
|
}
|
|
654
|
+
async #publicationTick(): Promise<void> {
|
|
655
|
+
// Before anything that reaches the native addon: the process guard may end the process here.
|
|
656
|
+
if (this.settings.beforePublicationTick()) return;
|
|
657
|
+
await this.#watchPublication();
|
|
658
|
+
if (this.#publicationState === "healthy-owned") await this.#stopIfIdle();
|
|
659
|
+
}
|
|
660
|
+
/** Record client activity; an active broker never idles out. */
|
|
661
|
+
noteActivity(): void {
|
|
662
|
+
this.#lastActivityAt = Date.now();
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Stop once nothing has used the broker for `idleShutdownMs`: no open client
|
|
666
|
+
* connection, no request or lifecycle chain in flight, and no live session host
|
|
667
|
+
* in the index. A stop from here is the ordinary owned-root stop, so discovery
|
|
668
|
+
* and the lock are released and the next `ensureBroker` starts a new broker.
|
|
669
|
+
*/
|
|
670
|
+
async #stopIfIdle(): Promise<void> {
|
|
671
|
+
const idleMs = this.settings.idleShutdownMs;
|
|
672
|
+
if (idleMs <= 0 || this.#stopping || this.#idleCheckInFlight) return;
|
|
673
|
+
if (!this.#idleCandidate(idleMs)) return;
|
|
674
|
+
this.#idleCheckInFlight = true;
|
|
675
|
+
try {
|
|
676
|
+
await this.index.refresh();
|
|
677
|
+
if (this.index.listSessions().sessions.some(session => session.live)) {
|
|
678
|
+
// Live hosts keep the broker; look again after another idle window.
|
|
679
|
+
this.#lastActivityAt = Date.now();
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
// Re-check: a client may have connected while the index was read.
|
|
683
|
+
if (this.#stopping || !this.#idleCandidate(idleMs)) return;
|
|
684
|
+
void this.#complete("owned-root");
|
|
685
|
+
} catch {
|
|
686
|
+
// An unreadable index is not evidence of idleness; try again next window.
|
|
687
|
+
this.#lastActivityAt = Date.now();
|
|
688
|
+
} finally {
|
|
689
|
+
this.#idleCheckInFlight = false;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
#idleCandidate(idleMs: number): boolean {
|
|
693
|
+
return (
|
|
694
|
+
Date.now() - this.#lastActivityAt >= idleMs &&
|
|
695
|
+
(this.#transport?.openConnections ?? 0) === 0 &&
|
|
696
|
+
this.#admitted.size === 0 &&
|
|
697
|
+
this.#chains.size === 0
|
|
698
|
+
);
|
|
699
|
+
}
|
|
628
700
|
async #watchPublication(writeHeartbeat = true): Promise<void> {
|
|
629
701
|
if (!this.#publication || this.#publicationState === "stopping") return;
|
|
630
702
|
let observation: ReturnType<RetainedBrokerDiscovery["observe"]>;
|
|
@@ -747,12 +819,14 @@ export class Broker {
|
|
|
747
819
|
handleRequest(operation: string, input: Record<string, unknown>, idempotencyKey?: string): Promise<BrokerResponse> {
|
|
748
820
|
if (this.#stopping || (this.#publication !== null && this.#publicationState !== "healthy-owned"))
|
|
749
821
|
return Promise.resolve(error("unavailable", "broker publication is unavailable"));
|
|
822
|
+
this.#lastActivityAt = Date.now();
|
|
750
823
|
let release!: () => void;
|
|
751
824
|
const admission = new Promise<void>(resolve => (release = resolve));
|
|
752
825
|
this.#admitted.add(admission);
|
|
753
826
|
return this.#handleRequest(operation, input, idempotencyKey).finally(() => {
|
|
754
827
|
release();
|
|
755
828
|
this.#admitted.delete(admission);
|
|
829
|
+
this.#lastActivityAt = Date.now();
|
|
756
830
|
});
|
|
757
831
|
}
|
|
758
832
|
async #handleRequest(
|