@sayknow-cli/coding-agent 0.5.10 → 0.5.11
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 +22 -0
- package/dist/types/config/model-profile-activation.d.ts +3 -1
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/tools/ask.d.ts +460 -172
- package/package.json +7 -7
- package/src/cli/telegram-cli.ts +1 -1
- package/src/config/model-profile-activation.ts +7 -11
- package/src/config/model-registry.ts +76 -4
- package/src/config/model-resolver.ts +4 -2
- package/src/defaults/skc/skills/deep-interview/SKILL.md +12 -0
- package/src/modes/components/settings-selector.ts +39 -21
- package/src/modes/controllers/selector-controller.ts +35 -15
- package/src/modes/rpc/rpc-client.ts +20 -4
- package/src/sdk/bus/index.ts +1 -3
- package/src/tools/ask.ts +366 -108
- package/src/tools/bisect.ts +4 -1
package/src/cli/telegram-cli.ts
CHANGED
|
@@ -148,7 +148,7 @@ export async function runTelegramCommand(cmd: TelegramCommandArgs): Promise<void
|
|
|
148
148
|
case "__gateway": {
|
|
149
149
|
// Hidden entrypoint: run the Telegram Remote gateway in-process. Reached
|
|
150
150
|
// only via the self-spawn from runStart()/autostart, never by users.
|
|
151
|
-
const { loadConfigFromEnv, runService } = await import("
|
|
151
|
+
const { loadConfigFromEnv, runService } = await import("@sayknow-cli/telegram-remote");
|
|
152
152
|
await runService(loadConfigFromEnv(process.env));
|
|
153
153
|
return;
|
|
154
154
|
}
|
|
@@ -2,11 +2,8 @@ import { ThinkingLevel } from "@sayknow-cli/agent-core";
|
|
|
2
2
|
import type { Api, Model } from "@sayknow-cli/ai";
|
|
3
3
|
import type { AgentSession } from "../session/agent-session";
|
|
4
4
|
import { formatClampedModelSelector } from "../thinking";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
formatAvailableProfileNames,
|
|
8
|
-
resolveProfileBindings,
|
|
9
|
-
} from "./model-profiles";
|
|
5
|
+
import { UnknownModelProfileError, validateModelProfileName } from "./model-profile-contract";
|
|
6
|
+
import { aggregateModelProfileRequiredProviders, resolveProfileBindings } from "./model-profiles";
|
|
10
7
|
import {
|
|
11
8
|
isAuthenticated,
|
|
12
9
|
kNoAuth,
|
|
@@ -49,7 +46,7 @@ export interface PrepareModelProfileActivationOptions {
|
|
|
49
46
|
| "resolveCanonicalModel"
|
|
50
47
|
| "getCanonicalVariants"
|
|
51
48
|
| "getCanonicalId"
|
|
52
|
-
|
|
49
|
+
> & { getError?: ModelRegistry["getError"] };
|
|
53
50
|
settings: Pick<Settings, "get">;
|
|
54
51
|
profileName: string;
|
|
55
52
|
}
|
|
@@ -347,12 +344,11 @@ export async function prepareModelProfileActivation(
|
|
|
347
344
|
options: PrepareModelProfileActivationOptions,
|
|
348
345
|
): Promise<PreparedModelProfileActivation> {
|
|
349
346
|
const profiles = options.modelRegistry.getModelProfiles();
|
|
350
|
-
|
|
347
|
+
// Typed contract errors (`unknown_model_profile` / `model_profile_registry_error`)
|
|
348
|
+
// so SDK lifecycle readiness and BrokerResponse preserve the code and details.
|
|
349
|
+
const profileName = validateModelProfileName(options.profileName, profiles, options.modelRegistry.getError?.());
|
|
351
350
|
const profile = profiles.get(profileName) ?? options.modelRegistry.getModelProfile(profileName);
|
|
352
|
-
if (!profile)
|
|
353
|
-
const available = formatAvailableProfileNames(profiles);
|
|
354
|
-
throw new Error(`Unknown model profile "${options.profileName}". Available profiles: ${available}`);
|
|
355
|
-
}
|
|
351
|
+
if (!profile) throw new UnknownModelProfileError(options.profileName, profiles);
|
|
356
352
|
const profileLabel = options.profileName;
|
|
357
353
|
|
|
358
354
|
const requiredProviders = aggregateModelProfileRequiredProviders(profile.requiredProviders, profile);
|
|
@@ -69,6 +69,36 @@ import { type Settings, settings } from "./settings";
|
|
|
69
69
|
|
|
70
70
|
export type { CanonicalModelIndex, CanonicalModelRecord, CanonicalModelVariant, ModelEquivalenceConfig };
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Strip userinfo and query strings from a discovery URL before it is surfaced
|
|
74
|
+
* in an error message, so credentials embedded in the URL never reach logs or UI.
|
|
75
|
+
*/
|
|
76
|
+
function redactDiscoveryUrl(value: string | URL): string {
|
|
77
|
+
try {
|
|
78
|
+
const url = typeof value === "string" ? new URL(value) : value;
|
|
79
|
+
return `${url.origin}${url.pathname}`;
|
|
80
|
+
} catch {
|
|
81
|
+
return "(invalid URL)";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Fingerprint of the environment variables that can change provider
|
|
87
|
+
* availability without touching AuthStorage. Keys the `getAvailable()` cache
|
|
88
|
+
* so a process.env mutation invalidates it.
|
|
89
|
+
*/
|
|
90
|
+
function envAvailabilityFingerprint(): string {
|
|
91
|
+
return Object.entries(process.env)
|
|
92
|
+
.filter(
|
|
93
|
+
([name]) =>
|
|
94
|
+
/(?:_API_KEY|_OAUTH_TOKEN|_ACCESS_TOKEN)$/.test(name) ||
|
|
95
|
+
/^(?:GH_TOKEN|GITHUB_TOKEN|HF_TOKEN|COPILOT_GITHUB_TOKEN)$/.test(name),
|
|
96
|
+
)
|
|
97
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
98
|
+
.map(([name, value]) => `${name}=${value ?? ""}`)
|
|
99
|
+
.join("\u0000");
|
|
100
|
+
}
|
|
101
|
+
|
|
72
102
|
export const kNoAuth = "N/A";
|
|
73
103
|
|
|
74
104
|
export function isAuthenticated(apiKey: string | undefined | null): apiKey is string {
|
|
@@ -544,6 +574,8 @@ export interface ProviderDiscoveryState {
|
|
|
544
574
|
export interface CanonicalModelQueryOptions {
|
|
545
575
|
availableOnly?: boolean;
|
|
546
576
|
candidates?: readonly Model<Api>[];
|
|
577
|
+
/** Session whose canonical stickiness should scope the lookup, when the caller has one. */
|
|
578
|
+
sessionId?: string;
|
|
547
579
|
}
|
|
548
580
|
|
|
549
581
|
/** Result of loading custom models from models.json */
|
|
@@ -1021,6 +1053,9 @@ export class ModelRegistry {
|
|
|
1021
1053
|
#providerWebSearchModes: Map<string, WebSearchMode> = new Map();
|
|
1022
1054
|
#keylessProviders: Set<string> = new Set();
|
|
1023
1055
|
#discoverableProviders: DiscoveryProviderConfig[] = [];
|
|
1056
|
+
#availableModelsCache: Model<Api>[] | undefined;
|
|
1057
|
+
#availableModelsDisabledProviders: string | undefined;
|
|
1058
|
+
#availableModelsEnvFingerprint: string | undefined;
|
|
1024
1059
|
#customModelOverlays: CustomModelOverlay[] = [];
|
|
1025
1060
|
#providerOverrides: Map<string, ProviderOverride> = new Map();
|
|
1026
1061
|
#modelOverrides: Map<string, Map<string, ModelOverride>> = new Map();
|
|
@@ -1074,6 +1109,8 @@ export class ModelRegistry {
|
|
|
1074
1109
|
const keyConfig = this.#customProviderApiKeys.get(provider);
|
|
1075
1110
|
return keyConfig;
|
|
1076
1111
|
});
|
|
1112
|
+
// Any credential mutation (runtime/config keys, OAuth refresh) changes availability.
|
|
1113
|
+
this.authStorage.onGenerationChanged(() => this.#invalidateAvailableModels());
|
|
1077
1114
|
// Load models synchronously in constructor
|
|
1078
1115
|
this.#loadModels();
|
|
1079
1116
|
}
|
|
@@ -2232,7 +2269,17 @@ export class ModelRegistry {
|
|
|
2232
2269
|
headers,
|
|
2233
2270
|
signal: AbortSignal.timeout(providerConfig.provider === "sglang" ? 500 : 250),
|
|
2234
2271
|
fetch: (input, init) => fetch(input, { ...init, redirect: "error" }),
|
|
2235
|
-
throwOnStatus: response =>
|
|
2272
|
+
throwOnStatus: response => {
|
|
2273
|
+
const modelsUrl = redactDiscoveryUrl(`${baseUrl}/models`);
|
|
2274
|
+
if (response.status === 401 || response.status === 403) {
|
|
2275
|
+
// Redacted by construction: name the provider, endpoint, and the
|
|
2276
|
+
// config surface to fix, never the resolved key.
|
|
2277
|
+
return new Error(
|
|
2278
|
+
`HTTP ${response.status} from ${modelsUrl}: provider "${providerConfig.provider}" credential was rejected for OpenAI models-list discovery; check providers.${providerConfig.provider}.apiKey/apiKeyEnv.`,
|
|
2279
|
+
);
|
|
2280
|
+
}
|
|
2281
|
+
return new Error(`HTTP ${response.status} from ${modelsUrl}`);
|
|
2282
|
+
},
|
|
2236
2283
|
mapModel: (item, defaults) => ({
|
|
2237
2284
|
...defaults,
|
|
2238
2285
|
reasoning: isOmlx,
|
|
@@ -2434,6 +2481,9 @@ export class ModelRegistry {
|
|
|
2434
2481
|
}
|
|
2435
2482
|
|
|
2436
2483
|
#rebuildCanonicalIndex(): void {
|
|
2484
|
+
// #models has already changed by the time a rebuild is requested; drop the
|
|
2485
|
+
// availability cache even when the index rebuild itself is deferred.
|
|
2486
|
+
this.#invalidateAvailableModels();
|
|
2437
2487
|
if (this.#rebuildSuspended > 0) {
|
|
2438
2488
|
this.#rebuildPending = true;
|
|
2439
2489
|
return;
|
|
@@ -2442,6 +2492,12 @@ export class ModelRegistry {
|
|
|
2442
2492
|
this.#rebuildPending = false;
|
|
2443
2493
|
}
|
|
2444
2494
|
|
|
2495
|
+
#invalidateAvailableModels(): void {
|
|
2496
|
+
this.#availableModelsCache = undefined;
|
|
2497
|
+
this.#availableModelsDisabledProviders = undefined;
|
|
2498
|
+
this.#availableModelsEnvFingerprint = undefined;
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2445
2501
|
#suspendRebuild(): void {
|
|
2446
2502
|
this.#rebuildSuspended += 1;
|
|
2447
2503
|
}
|
|
@@ -2453,6 +2509,7 @@ export class ModelRegistry {
|
|
|
2453
2509
|
if (this.#rebuildSuspended === 0 && this.#rebuildPending) {
|
|
2454
2510
|
this.#rebuildPending = false;
|
|
2455
2511
|
this.#canonicalIndex = buildCanonicalModelIndex(this.#models, this.#equivalenceConfig);
|
|
2512
|
+
this.#invalidateAvailableModels();
|
|
2456
2513
|
}
|
|
2457
2514
|
}
|
|
2458
2515
|
|
|
@@ -2503,8 +2560,10 @@ export class ModelRegistry {
|
|
|
2503
2560
|
return this.#models;
|
|
2504
2561
|
}
|
|
2505
2562
|
|
|
2506
|
-
#isModelAvailable(
|
|
2507
|
-
|
|
2563
|
+
#isModelAvailable(
|
|
2564
|
+
model: Model<Api>,
|
|
2565
|
+
disabledProviders: ReadonlySet<string> = getDisabledProviderIdsFromSettings(),
|
|
2566
|
+
): boolean {
|
|
2508
2567
|
return (
|
|
2509
2568
|
!disabledProviders.has(model.provider) &&
|
|
2510
2569
|
(this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider))
|
|
@@ -2643,7 +2702,20 @@ export class ModelRegistry {
|
|
|
2643
2702
|
* This is a fast check that doesn't refresh OAuth tokens.
|
|
2644
2703
|
*/
|
|
2645
2704
|
getAvailable(): Model<Api>[] {
|
|
2646
|
-
|
|
2705
|
+
const disabledProviders = getDisabledProviderIdsFromSettings();
|
|
2706
|
+
const disabledProviderKey = [...disabledProviders].sort().join("\u0000");
|
|
2707
|
+
const envFingerprint = envAvailabilityFingerprint();
|
|
2708
|
+
if (
|
|
2709
|
+
this.#availableModelsCache &&
|
|
2710
|
+
this.#availableModelsDisabledProviders === disabledProviderKey &&
|
|
2711
|
+
this.#availableModelsEnvFingerprint === envFingerprint
|
|
2712
|
+
) {
|
|
2713
|
+
return this.#availableModelsCache;
|
|
2714
|
+
}
|
|
2715
|
+
this.#availableModelsCache = this.#models.filter(model => this.#isModelAvailable(model, disabledProviders));
|
|
2716
|
+
this.#availableModelsDisabledProviders = disabledProviderKey;
|
|
2717
|
+
this.#availableModelsEnvFingerprint = envFingerprint;
|
|
2718
|
+
return this.#availableModelsCache;
|
|
2647
2719
|
}
|
|
2648
2720
|
|
|
2649
2721
|
/**
|
|
@@ -355,7 +355,7 @@ function findExactCanonicalModelMatch(
|
|
|
355
355
|
modelReference: string,
|
|
356
356
|
availableModels: Model<Api>[],
|
|
357
357
|
modelRegistry: CanonicalModelRegistry | undefined,
|
|
358
|
-
|
|
358
|
+
sessionId?: string,
|
|
359
359
|
): Model<Api> | undefined {
|
|
360
360
|
if (!modelRegistry) {
|
|
361
361
|
return undefined;
|
|
@@ -367,6 +367,7 @@ function findExactCanonicalModelMatch(
|
|
|
367
367
|
return modelRegistry.resolveCanonicalModel?.(trimmedReference, {
|
|
368
368
|
availableOnly: false,
|
|
369
369
|
candidates: availableModels,
|
|
370
|
+
sessionId,
|
|
370
371
|
});
|
|
371
372
|
}
|
|
372
373
|
|
|
@@ -378,7 +379,7 @@ function findExactEquivalentModelMatch(
|
|
|
378
379
|
modelReference: string,
|
|
379
380
|
availableModels: Model<Api>[],
|
|
380
381
|
modelRegistry: CanonicalModelRegistry | undefined,
|
|
381
|
-
|
|
382
|
+
sessionId?: string,
|
|
382
383
|
): Model<Api> | undefined {
|
|
383
384
|
if (!modelRegistry?.getCanonicalId || !modelRegistry.resolveCanonicalModel) return undefined;
|
|
384
385
|
const trimmedReference = modelReference.trim();
|
|
@@ -394,6 +395,7 @@ function findExactEquivalentModelMatch(
|
|
|
394
395
|
return modelRegistry.resolveCanonicalModel([...canonicalIds][0]!, {
|
|
395
396
|
availableOnly: false,
|
|
396
397
|
candidates: availableModels,
|
|
398
|
+
sessionId,
|
|
397
399
|
});
|
|
398
400
|
}
|
|
399
401
|
|
|
@@ -635,6 +635,18 @@ A transition occurs whenever the band changes versus the prior scored round —
|
|
|
635
635
|
|
|
636
636
|
**Bookkeeping:** record each convened panel in `state.lateral_reviews` (round, milestone transition or pre-answer trigger, personas dispatched, findings folded). On panel spawn or validation failure, fall back silently to the normal generated question and increment `lateral_panel_failures`; do not expose tool noise unless it changes the next user-facing question. The panel is a prompt-budgeted assist layer — summarize oversized context before dispatch.
|
|
637
637
|
|
|
638
|
+
### Per-question advisory fanout lanes (distinct from the milestone panel)
|
|
639
|
+
|
|
640
|
+
Separate from the milestone-triggered lateral panel above, a lightweight **advisory fanout** may assist any single question the main session is about to synthesize or route — especially when the user is terse, uncertain, or would benefit from selectable options instead of another open-ended prompt. Adopted from ouroboros's ooo interview, the standard lanes are:
|
|
641
|
+
|
|
642
|
+
- `code_context` — inspect repo-local facts and reuse existing exploration before asking the user.
|
|
643
|
+
- `web_context` — browse/search only when current external facts genuinely affect the answer.
|
|
644
|
+
- `ambiguity_contrarian` — find hidden assumptions, vague terms, missing decisions, and risky defaults.
|
|
645
|
+
- `answer_simplifier` — turn the question into 2-3 easy choices or one concise draft answer.
|
|
646
|
+
- `architecture_implications` — check whether the answer changes ownership, interfaces, rollout, or system shape.
|
|
647
|
+
|
|
648
|
+
Advisory fanout is an assist layer, not a decision maker: it never replaces or delays the single user-facing question, never adds a second question, and never forwards a synthesized answer without the user's approval, edit, or explicit auto-confirm request. It differs from the milestone panel in trigger (per-question, not band-transition) and intent (help the human answer this one question). When both would fire on the same round, run the milestone panel and fold advisory lanes into the same single question. Runtimes without a parallel subagent primitive process lanes sequentially; on lane failure, fall back silently to the normal generated question.
|
|
649
|
+
|
|
638
650
|
## Phase 4: Crystallize Spec
|
|
639
651
|
|
|
640
652
|
When ambiguity ≤ threshold (or hard cap / early exit):
|
|
@@ -204,6 +204,10 @@ const TIME_FORMAT_OPTIONS: SelectItem[] = [
|
|
|
204
204
|
{ value: "24h", label: "24h" },
|
|
205
205
|
{ value: "12h", label: "12h" },
|
|
206
206
|
];
|
|
207
|
+
const USAGE_MODE_OPTIONS: SelectItem[] = [
|
|
208
|
+
{ value: "used", label: "Used" },
|
|
209
|
+
{ value: "remaining", label: "Remaining" },
|
|
210
|
+
];
|
|
207
211
|
const USAGE_MODE_VALUES = ["used", "remaining"] as const;
|
|
208
212
|
type UsageMode = (typeof USAGE_MODE_VALUES)[number];
|
|
209
213
|
|
|
@@ -220,6 +224,7 @@ function mergeSegmentOptions(
|
|
|
220
224
|
path: base?.path || overrides?.path ? { ...(base?.path ?? {}), ...(overrides?.path ?? {}) } : undefined,
|
|
221
225
|
git: base?.git || overrides?.git ? { ...(base?.git ?? {}), ...(overrides?.git ?? {}) } : undefined,
|
|
222
226
|
time: base?.time || overrides?.time ? { ...(base?.time ?? {}), ...(overrides?.time ?? {}) } : undefined,
|
|
227
|
+
usage: base?.usage || overrides?.usage ? { ...(base?.usage ?? {}), ...(overrides?.usage ?? {}) } : undefined,
|
|
223
228
|
};
|
|
224
229
|
}
|
|
225
230
|
|
|
@@ -279,8 +284,6 @@ function segmentPlacement(
|
|
|
279
284
|
class StatusLineCustomEditor extends Container {
|
|
280
285
|
#list!: SettingsList;
|
|
281
286
|
#draft: StatusLineDraft;
|
|
282
|
-
#currentWidthPreviewText!: Text;
|
|
283
|
-
#narrowWidthPreviewText!: Text;
|
|
284
287
|
#previewHighlightSegment: StatusLineSegmentId | undefined;
|
|
285
288
|
|
|
286
289
|
constructor(
|
|
@@ -312,13 +315,6 @@ class StatusLineCustomEditor extends Container {
|
|
|
312
315
|
this.clear();
|
|
313
316
|
this.addChild(new Text(theme.bold(theme.fg("accent", "Status Line Custom Editor")), 0, 0));
|
|
314
317
|
this.addChild(new Spacer(1));
|
|
315
|
-
this.addChild(new Text(theme.fg("muted", "Current width preview:"), 0, 0));
|
|
316
|
-
this.#currentWidthPreviewText = new Text(this.#statusLinePreview(), 0, 0);
|
|
317
|
-
this.addChild(this.#currentWidthPreviewText);
|
|
318
|
-
this.addChild(new Text(theme.fg("muted", "Narrow width preview:"), 0, 0));
|
|
319
|
-
this.#narrowWidthPreviewText = new Text(this.#statusLinePreview(40), 0, 0);
|
|
320
|
-
this.addChild(this.#narrowWidthPreviewText);
|
|
321
|
-
this.addChild(new Spacer(1));
|
|
322
318
|
this.#list = new SettingsList(
|
|
323
319
|
this.#items(),
|
|
324
320
|
14,
|
|
@@ -326,27 +322,17 @@ class StatusLineCustomEditor extends Container {
|
|
|
326
322
|
(id, value) => this.#handleChange(id, value),
|
|
327
323
|
() => this.#cancel(),
|
|
328
324
|
item => this.#setSelectedItem(item),
|
|
325
|
+
2,
|
|
329
326
|
);
|
|
330
327
|
this.addChild(this.#list);
|
|
331
328
|
}
|
|
332
329
|
|
|
333
330
|
#refresh(): void {
|
|
334
|
-
this.#refreshPreview();
|
|
335
331
|
this.#list.setItems(this.#items());
|
|
336
332
|
}
|
|
337
|
-
|
|
338
|
-
#refreshPreview(): void {
|
|
339
|
-
this.#currentWidthPreviewText.setText(this.#statusLinePreview());
|
|
340
|
-
this.#narrowWidthPreviewText.setText(this.#statusLinePreview(40));
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
#statusLinePreview(width?: number): string {
|
|
344
|
-
return this.callbacks.getStatusLinePreview?.(width) ?? theme.fg("dim", "(preview not available)");
|
|
345
|
-
}
|
|
346
333
|
#setSelectedItem(item: SettingItem | undefined): void {
|
|
347
334
|
this.#previewHighlightSegment = this.#highlightSegmentForItem(item);
|
|
348
335
|
this.#preview();
|
|
349
|
-
this.#refreshPreview();
|
|
350
336
|
}
|
|
351
337
|
|
|
352
338
|
#highlightSegmentForItem(item: SettingItem | undefined): StatusLineSegmentId | undefined {
|
|
@@ -432,6 +418,22 @@ class StatusLineCustomEditor extends Container {
|
|
|
432
418
|
},
|
|
433
419
|
);
|
|
434
420
|
}
|
|
421
|
+
if (id === "usage") {
|
|
422
|
+
items.push({
|
|
423
|
+
id: "option.usage.mode",
|
|
424
|
+
label: "Usage: mode",
|
|
425
|
+
currentValue: this.#draft.segmentOptions.usage?.mode ?? "used",
|
|
426
|
+
submenu: (currentValue, done) =>
|
|
427
|
+
new SelectSubmenu(
|
|
428
|
+
"Usage mode",
|
|
429
|
+
"Show used quota or remaining quota in the usage segment.",
|
|
430
|
+
USAGE_MODE_OPTIONS,
|
|
431
|
+
currentValue,
|
|
432
|
+
done,
|
|
433
|
+
() => done(),
|
|
434
|
+
),
|
|
435
|
+
});
|
|
436
|
+
}
|
|
435
437
|
}
|
|
436
438
|
|
|
437
439
|
items.push(
|
|
@@ -596,6 +598,12 @@ class StatusLineCustomEditor extends Container {
|
|
|
596
598
|
case "time.showSeconds":
|
|
597
599
|
this.#draft.segmentOptions.time = { ...(this.#draft.segmentOptions.time ?? {}), showSeconds: bool };
|
|
598
600
|
break;
|
|
601
|
+
case "usage.mode":
|
|
602
|
+
this.#draft.segmentOptions.usage = {
|
|
603
|
+
...(this.#draft.segmentOptions.usage ?? {}),
|
|
604
|
+
mode: value === "remaining" ? "remaining" : "used",
|
|
605
|
+
};
|
|
606
|
+
break;
|
|
599
607
|
}
|
|
600
608
|
}
|
|
601
609
|
|
|
@@ -606,6 +614,8 @@ class StatusLineCustomEditor extends Container {
|
|
|
606
614
|
rightSegments: [...this.#draft.rightSegments],
|
|
607
615
|
separator: this.#draft.separator,
|
|
608
616
|
segmentOptions: cloneSegmentOptions(this.#draft.segmentOptions),
|
|
617
|
+
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
618
|
+
maxRows: settings.get("statusLine.maxRows"),
|
|
609
619
|
previewHighlightSegment: this.#previewHighlightSegment,
|
|
610
620
|
});
|
|
611
621
|
}
|
|
@@ -618,6 +628,7 @@ class StatusLineCustomEditor extends Container {
|
|
|
618
628
|
separator: settings.get("statusLine.separator"),
|
|
619
629
|
segmentOptions: cloneSegmentOptions(settings.get("statusLine.segmentOptions") as StatusLineSegmentOptions),
|
|
620
630
|
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
631
|
+
maxRows: settings.get("statusLine.maxRows"),
|
|
621
632
|
previewHighlightSegment: undefined,
|
|
622
633
|
});
|
|
623
634
|
}
|
|
@@ -1197,13 +1208,20 @@ export class SettingsSelectorComponent extends Container {
|
|
|
1197
1208
|
#buildItemsForTab(defs: SettingDef[], tabId: SettingTab): SettingItem[] {
|
|
1198
1209
|
const items = this.#buildItemsForDefs(defs);
|
|
1199
1210
|
if (tabId === "appearance") {
|
|
1211
|
+
const customEditorCallbacks: SettingsCallbacks = {
|
|
1212
|
+
...this.callbacks,
|
|
1213
|
+
onStatusLinePreview: previewSettings => {
|
|
1214
|
+
this.callbacks.onStatusLinePreview?.(previewSettings);
|
|
1215
|
+
this.#updateStatusPreview();
|
|
1216
|
+
},
|
|
1217
|
+
};
|
|
1200
1218
|
const customEditorItem: SettingItem = {
|
|
1201
1219
|
id: STATUS_LINE_CUSTOM_EDITOR_ID,
|
|
1202
1220
|
label: "Status Line Custom Editor",
|
|
1203
1221
|
description:
|
|
1204
1222
|
"Edit custom status line segments, placement, separator, and typed segment options with live previews.",
|
|
1205
1223
|
currentValue: "open",
|
|
1206
|
-
submenu: (_currentValue, done) => new StatusLineCustomEditor(
|
|
1224
|
+
submenu: (_currentValue, done) => new StatusLineCustomEditor(customEditorCallbacks, done),
|
|
1207
1225
|
};
|
|
1208
1226
|
const presetIndex = items.findIndex(item => item.id === "statusLine.preset");
|
|
1209
1227
|
if (presetIndex >= 0) {
|
|
@@ -5,7 +5,7 @@ import type { OAuthProvider } from "@sayknow-cli/ai/utils/oauth/types";
|
|
|
5
5
|
import type { Component, OverlayHandle, SlashCommand } from "@sayknow-cli/tui";
|
|
6
6
|
import { Input, isPetMode, Loader, Spacer, Text } from "@sayknow-cli/tui";
|
|
7
7
|
import { getAgentDbPath, getProjectDir, logger, VERSION } from "@sayknow-cli/utils";
|
|
8
|
-
import {
|
|
8
|
+
import type { AppKeybinding } from "../../config/keybindings";
|
|
9
9
|
import {
|
|
10
10
|
activateModelProfile,
|
|
11
11
|
materializeActiveModelProfileAssignment,
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
getPluginsCacheDir,
|
|
28
28
|
MarketplaceManager,
|
|
29
29
|
} from "../../extensibility/plugins/marketplace";
|
|
30
|
+
import { INTERACTIVE_SELECTOR_RESUME_ORIGIN } from "../../extensibility/shared-events";
|
|
30
31
|
import {
|
|
31
32
|
getAvailableThemes,
|
|
32
33
|
getCurrentThemeName,
|
|
@@ -832,7 +833,7 @@ export class SelectorController {
|
|
|
832
833
|
id: `action:${action.id}`,
|
|
833
834
|
label: action.label,
|
|
834
835
|
description: action.id,
|
|
835
|
-
keybinding:
|
|
836
|
+
keybinding: this.ctx.keybindings.getDisplayString(action.id as AppKeybinding) || undefined,
|
|
836
837
|
searchText: action.id,
|
|
837
838
|
handler: action.handler,
|
|
838
839
|
})),
|
|
@@ -1042,13 +1043,18 @@ export class SelectorController {
|
|
|
1042
1043
|
const selector = new ThinkingSelectorComponent(
|
|
1043
1044
|
this.ctx.session.thinkingLevel,
|
|
1044
1045
|
availableLevels,
|
|
1045
|
-
selection => {
|
|
1046
|
-
done();
|
|
1047
|
-
|
|
1046
|
+
async selection => {
|
|
1048
1047
|
const { level, persistDefault } = selection;
|
|
1049
1048
|
const configuredDefault = this.ctx.settings.get("defaultThinkingLevel");
|
|
1050
1049
|
const levelToApply = level === ThinkingLevel.Inherit ? configuredDefault : level;
|
|
1051
|
-
|
|
1050
|
+
try {
|
|
1051
|
+
await this.ctx.session.setThinkingLevelForControl(level, persistDefault);
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
done();
|
|
1057
|
+
|
|
1052
1058
|
const effectiveLevel = this.ctx.session.thinkingLevel ?? ThinkingLevel.Off;
|
|
1053
1059
|
const requestedLabel =
|
|
1054
1060
|
level === ThinkingLevel.Inherit ? `${level} (configured default: ${configuredDefault})` : level;
|
|
@@ -1959,10 +1965,7 @@ export class SelectorController {
|
|
|
1959
1965
|
}
|
|
1960
1966
|
|
|
1961
1967
|
async showSessionSelector(): Promise<void> {
|
|
1962
|
-
const sessions = await
|
|
1963
|
-
this.ctx.sessionManager.getCwd(),
|
|
1964
|
-
this.ctx.sessionManager.getSessionDir(),
|
|
1965
|
-
);
|
|
1968
|
+
const sessions = await this.ctx.sessionManager.listForResumePickerReadOnly();
|
|
1966
1969
|
this.showSelector(done => {
|
|
1967
1970
|
const selector = new SessionSelectorComponent(
|
|
1968
1971
|
sessions,
|
|
@@ -2064,13 +2067,30 @@ export class SelectorController {
|
|
|
2064
2067
|
const previousSessionId = this.ctx.sessionManager.getSessionId();
|
|
2065
2068
|
const migrationPolicy =
|
|
2066
2069
|
this.ctx.settings?.get("session.directoryMigration") === "disabled" ? "disabled" : "copy-retain";
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2070
|
+
let writableSessionPath = sessionPath;
|
|
2071
|
+
if (this.ctx.sessionManager.isManagedDestination()) {
|
|
2072
|
+
// Managed candidates are prepared against their inspected identity so a
|
|
2073
|
+
// replacement race fails closed before adoption. Explicit `--session-dir`
|
|
2074
|
+
// selections never enter the managed migration fence (preparation would
|
|
2075
|
+
// reject a non-managed destination authority outright).
|
|
2076
|
+
const inspection = await SessionManager.inspectSessionTailReadOnly(sessionPath);
|
|
2077
|
+
if (inspection.kind === "error")
|
|
2078
|
+
throw new Error(`Could not inspect selected session: ${inspection.reason}`);
|
|
2079
|
+
writableSessionPath = await this.ctx.sessionManager.prepareManagedCandidateForStrictAdoption(
|
|
2080
|
+
sessionPath,
|
|
2081
|
+
migrationPolicy,
|
|
2082
|
+
inspection.identity,
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2071
2085
|
|
|
2072
2086
|
// Switch session via AgentSession (emits hook and tool session events)
|
|
2073
|
-
if (
|
|
2087
|
+
if (
|
|
2088
|
+
!(await this.ctx.session.switchSession(writableSessionPath, {
|
|
2089
|
+
...options,
|
|
2090
|
+
transition: { origin: INTERACTIVE_SELECTOR_RESUME_ORIGIN },
|
|
2091
|
+
}))
|
|
2092
|
+
)
|
|
2093
|
+
return false;
|
|
2074
2094
|
onSwitched?.();
|
|
2075
2095
|
this.#clearTransientSessionUi();
|
|
2076
2096
|
const switchingToDifferentSession = previousSessionId !== this.ctx.sessionManager.getSessionId();
|
|
@@ -288,11 +288,27 @@ class StdioRpcTransport implements RpcTransport {
|
|
|
288
288
|
} else {
|
|
289
289
|
onClose(new Error("RPC stdio transport closed"));
|
|
290
290
|
}
|
|
291
|
-
})().catch((err: Error) => {
|
|
292
|
-
if (
|
|
293
|
-
|
|
291
|
+
})().catch(async (err: Error) => {
|
|
292
|
+
if (readySettled) {
|
|
293
|
+
onClose(err);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
// Non-JSONL stdout before `ready` usually means the CLI refused to start
|
|
297
|
+
// (for example a usage error such as the removed `--mode rpc`). When the
|
|
298
|
+
// child is already gone, report its exit code and stderr instead of the
|
|
299
|
+
// parse noise so callers see the actual refusal.
|
|
300
|
+
const proc = this.#process;
|
|
301
|
+
const exitCode = proc
|
|
302
|
+
? await Promise.race([proc.exited.catch(() => proc.exitCode ?? -1), Bun.sleep(1_000).then(() => undefined)])
|
|
303
|
+
: undefined;
|
|
304
|
+
if (readySettled) return;
|
|
305
|
+
readySettled = true;
|
|
306
|
+
if (exitCode === undefined) {
|
|
294
307
|
readyReject(err);
|
|
295
|
-
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const stderr = await getStartupStderr();
|
|
311
|
+
readyReject(new Error(`Agent process exited with code ${exitCode} before ready. Stderr: ${stderr}`));
|
|
296
312
|
});
|
|
297
313
|
void this.#process.exited.then(async (exitCode: number) => {
|
|
298
314
|
if (!readySettled) {
|
package/src/sdk/bus/index.ts
CHANGED
|
@@ -3217,9 +3217,7 @@ export function createNotificationsExtension(
|
|
|
3217
3217
|
let serverStopped = rt.serverStopped;
|
|
3218
3218
|
if (!serverStopped) {
|
|
3219
3219
|
try {
|
|
3220
|
-
|
|
3221
|
-
// async variant. TODO(port): add stop_and_wait to pi-natives/src/notifications.rs.
|
|
3222
|
-
rt.server.stop();
|
|
3220
|
+
await rt.server.stopAndWait();
|
|
3223
3221
|
serverStopped = true;
|
|
3224
3222
|
rt.serverStopped = true;
|
|
3225
3223
|
} catch (e) {
|