@sayknow-cli/coding-agent 0.6.6 → 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 +19 -0
- package/dist/types/config/settings-schema.d.ts +79 -0
- package/dist/types/i18n/messages/en.d.ts +2 -0
- package/dist/types/modes/components/welcome.d.ts +21 -4
- package/dist/types/modes/interactive-mode.d.ts +2 -5
- package/dist/types/modes/types.d.ts +2 -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/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/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 +2 -0
- package/src/i18n/messages/en.ts +2 -0
- package/src/i18n/messages/es.settings.ts +26 -0
- package/src/i18n/messages/es.ts +2 -0
- package/src/i18n/messages/fr.settings.ts +26 -0
- package/src/i18n/messages/fr.ts +2 -0
- package/src/i18n/messages/ja.settings.ts +26 -0
- package/src/i18n/messages/ja.ts +2 -0
- package/src/i18n/messages/ko.settings.ts +25 -0
- package/src/i18n/messages/ko.ts +2 -0
- package/src/i18n/messages/zh.settings.ts +23 -0
- package/src/i18n/messages/zh.ts +2 -0
- package/src/internal-urls/docs-index.generated.ts +7 -6
- package/src/modes/components/welcome.ts +240 -68
- package/src/modes/controllers/input-controller.ts +2 -0
- package/src/modes/controllers/selector-controller.ts +13 -0
- package/src/modes/interactive-mode.ts +118 -14
- package/src/modes/types.ts +2 -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
|
@@ -16,6 +16,8 @@ import { theme } from "../../modes/theme/theme";
|
|
|
16
16
|
export interface RecentSession {
|
|
17
17
|
name: string;
|
|
18
18
|
timeAgo: string;
|
|
19
|
+
/** Session file; present when the row can be opened from the card. */
|
|
20
|
+
path?: string;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -49,21 +51,34 @@ export interface WelcomeComponentOptions {
|
|
|
49
51
|
continueKey?: string;
|
|
50
52
|
/** Which Sayknow pet stands on the card (default red). */
|
|
51
53
|
petSkin?: PetSkinId;
|
|
54
|
+
/** Called when a session row is opened by click or Enter. */
|
|
55
|
+
onOpenSession?: (session: RecentSession) => void;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
/** Left margin of the card, and the widest the card grows on wide terminals. */
|
|
55
59
|
const MARGIN = 2;
|
|
56
60
|
const MAX_CARD_WIDTH = 76;
|
|
57
|
-
/** Gap between the
|
|
61
|
+
/** Gap between the pet and the wordmark. */
|
|
58
62
|
const MARK_GAP = 3;
|
|
59
|
-
/** Below this card width the mark is dropped and only the identity lines remain. */
|
|
60
|
-
const MIN_WIDTH_FOR_MARK = 44;
|
|
61
63
|
/** Recent sessions on the card; the resume picker has the rest. */
|
|
62
64
|
const SESSION_ROWS = 3;
|
|
63
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The wordmark: SAYKNOW in the brand's box-drawing letters, then CLI. Each row is the
|
|
68
|
+
* SAYKNOW part and the CLI part, drawn in different colors.
|
|
69
|
+
*/
|
|
70
|
+
// biome-ignore format: preserve letter layout
|
|
71
|
+
const WORDMARK: ReadonlyArray<readonly [string, string]> = [
|
|
72
|
+
["╔═╗╔═╗╦ ╦╦╔═╔╗╔╔═╗╦ ╦", "╔═╗╦ ╦"],
|
|
73
|
+
["╚═╗╠═╣╚╦╝╠╩╗║║║║ ║║║║", "║ ║ ║"],
|
|
74
|
+
["╚═╝╩ ╩ ╩ ╩ ╩╝╚╝╚═╝╚╩╝", "╚═╝╩═╝╩"],
|
|
75
|
+
];
|
|
76
|
+
const WORDMARK_GAP = 2;
|
|
77
|
+
const WORDMARK_WIDTH = 21 + WORDMARK_GAP + 7;
|
|
78
|
+
|
|
64
79
|
/** ASCII-safe stand-in for the pet when the banner must avoid block glyphs. */
|
|
65
|
-
const MARK_ASCII = ["
|
|
66
|
-
const MARK_ASCII_WAVE = "
|
|
80
|
+
const MARK_ASCII = [" .--. ", "( oo )", " ) ( ", "/\\/\\/\\"] as const;
|
|
81
|
+
const MARK_ASCII_WAVE = "\\/\\/\\/";
|
|
67
82
|
|
|
68
83
|
/**
|
|
69
84
|
* Intro: the pet does a short para-para (tentacles sway left, right, left) and
|
|
@@ -80,18 +95,25 @@ interface Section {
|
|
|
80
95
|
lines: string[];
|
|
81
96
|
/** When the viewport is short, the section with the highest rank is dropped first. */
|
|
82
97
|
dropRank: number;
|
|
98
|
+
/** Session index per line, for rows that open a session. */
|
|
99
|
+
sessionRows?: Array<number | undefined>;
|
|
83
100
|
}
|
|
84
101
|
|
|
85
102
|
/**
|
|
86
|
-
* Sayknow-CLI launch card: the
|
|
87
|
-
* (version, model and reasoning, project and branch),
|
|
88
|
-
*
|
|
89
|
-
*
|
|
103
|
+
* Sayknow-CLI launch card: the wordmark with the pet beside it, who and where you are
|
|
104
|
+
* (version, model and reasoning, project and branch), the release line after an update,
|
|
105
|
+
* the three most recent sessions — which can be opened from the card with a click or
|
|
106
|
+
* ↓ then Enter — and a single row of keys.
|
|
90
107
|
*/
|
|
91
108
|
export class WelcomeComponent implements Component {
|
|
92
109
|
#animStart: number | null = null;
|
|
93
110
|
#animTimer: NodeJS.Timeout | null = null;
|
|
94
111
|
#snapshot: WelcomeSnapshot;
|
|
112
|
+
/** Highlighted session row while picking with the keyboard; undefined when not picking. */
|
|
113
|
+
#selected: number | undefined;
|
|
114
|
+
/** Session index per rendered line, from the last frame. */
|
|
115
|
+
#lineSessions: Array<number | undefined> = [];
|
|
116
|
+
#interactive = true;
|
|
95
117
|
|
|
96
118
|
constructor(
|
|
97
119
|
private readonly version: string,
|
|
@@ -145,6 +167,7 @@ export class WelcomeComponent implements Component {
|
|
|
145
167
|
|
|
146
168
|
setRecentSessions(sessions: RecentSession[]): void {
|
|
147
169
|
this.recentSessions = sessions;
|
|
170
|
+
if (this.#selected !== undefined && this.#selected >= this.#openable().length) this.#selected = undefined;
|
|
148
171
|
}
|
|
149
172
|
|
|
150
173
|
/** Merge newly probed workspace facts into the card. */
|
|
@@ -152,7 +175,78 @@ export class WelcomeComponent implements Component {
|
|
|
152
175
|
this.#snapshot = { ...this.#snapshot, ...patch };
|
|
153
176
|
}
|
|
154
177
|
|
|
178
|
+
// ── Picking a session ───────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
/** Sessions on the card that can be opened (they have a file). */
|
|
181
|
+
#openable(): RecentSession[] {
|
|
182
|
+
return this.recentSessions.slice(0, SESSION_ROWS).filter(session => session.path);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Rows that can be opened right now. Zero once the card stops taking input. */
|
|
186
|
+
get openableCount(): number {
|
|
187
|
+
return this.#interactive ? this.#openable().length : 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
get selectedIndex(): number | undefined {
|
|
191
|
+
return this.#selected;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Stop offering the session rows (after the first prompt or a session switch). */
|
|
195
|
+
endInteraction(): void {
|
|
196
|
+
this.#interactive = false;
|
|
197
|
+
this.#selected = undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
get interactive(): boolean {
|
|
201
|
+
return this.#interactive;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Highlight a row, or clear the highlight with undefined. Returns false when there is nothing to select. */
|
|
205
|
+
select(index: number | undefined): boolean {
|
|
206
|
+
if (index === undefined) {
|
|
207
|
+
this.#selected = undefined;
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
const count = this.openableCount;
|
|
211
|
+
if (count === 0) return false;
|
|
212
|
+
this.#selected = Math.max(0, Math.min(count - 1, index));
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Move the highlight; returns false when it would leave the list upward (the caller hands focus back). */
|
|
217
|
+
moveSelection(delta: number): boolean {
|
|
218
|
+
if (this.#selected === undefined) return this.select(0);
|
|
219
|
+
const next = this.#selected + delta;
|
|
220
|
+
if (next < 0) {
|
|
221
|
+
this.#selected = undefined;
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
this.#selected = Math.min(this.openableCount - 1, next);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Open the highlighted row. */
|
|
229
|
+
openSelected(): boolean {
|
|
230
|
+
const session = this.#selected === undefined ? undefined : this.#openable()[this.#selected];
|
|
231
|
+
if (!session) return false;
|
|
232
|
+
this.options.onOpenSession?.(session);
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
handleClick(line: number): boolean {
|
|
237
|
+
if (!this.#interactive) return false;
|
|
238
|
+
const index = this.#lineSessions[line];
|
|
239
|
+
const session = index === undefined ? undefined : this.#openable()[index];
|
|
240
|
+
if (!session) return false;
|
|
241
|
+
this.#selected = index;
|
|
242
|
+
this.options.onOpenSession?.(session);
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── Layout ──────────────────────────────────────────────────────────────
|
|
247
|
+
|
|
155
248
|
render(termWidth: number): string[] {
|
|
249
|
+
this.#lineSessions = [];
|
|
156
250
|
const gutterWidth = this.#rightGutterWidth(termWidth);
|
|
157
251
|
const width = Math.max(0, termWidth - gutterWidth);
|
|
158
252
|
if (width < 4) return [];
|
|
@@ -163,76 +257,112 @@ export class WelcomeComponent implements Component {
|
|
|
163
257
|
|
|
164
258
|
const cardWidth = Math.max(1, Math.min(MAX_CARD_WIDTH, width - MARGIN));
|
|
165
259
|
const sections = this.#sections(cardWidth);
|
|
166
|
-
const lines = this.#fitRows(sections, targetRows);
|
|
260
|
+
const { lines, sessionRows } = this.#fitRows(sections, targetRows);
|
|
167
261
|
const margin = padding(MARGIN);
|
|
168
|
-
const
|
|
262
|
+
const selectedLine = sessionRows.findIndex(index => index !== undefined && index === this.#selected);
|
|
263
|
+
const out = lines.map((line, row) => {
|
|
264
|
+
if (row === selectedLine) return this.#highlightRow(line, cardWidth, width);
|
|
265
|
+
return this.#fit(line ? margin + line : "", width);
|
|
266
|
+
});
|
|
169
267
|
if (targetRows !== undefined) while (out.length < targetRows) out.push(padding(width));
|
|
268
|
+
this.#lineSessions = sessionRows;
|
|
170
269
|
return this.#withRightGutter(out, gutterWidth);
|
|
171
270
|
}
|
|
172
271
|
|
|
272
|
+
/** The picked row: a selection band across the card width, held after every reset inside the row. */
|
|
273
|
+
#highlightRow(line: string, cardWidth: number, width: number): string {
|
|
274
|
+
const bg = theme.getBgAnsi("selectedBg");
|
|
275
|
+
const body = this.#fit(line, cardWidth).replace(/\x1b\[(?:0)?m|\x1b\[49m/g, match => `${match}${bg}`);
|
|
276
|
+
return this.#fit(`${padding(MARGIN)}${bg}${body}\x1b[0m`, width);
|
|
277
|
+
}
|
|
278
|
+
|
|
173
279
|
/**
|
|
174
280
|
* Lay the sections out with a blank row above and between them. When the
|
|
175
281
|
* viewport is short, whole sections go by rank — keys, then the release
|
|
176
282
|
* note, then sessions — and the identity block is clipped last.
|
|
177
283
|
*/
|
|
178
|
-
#fitRows(
|
|
284
|
+
#fitRows(
|
|
285
|
+
sections: { identity: string[]; others: Section[] },
|
|
286
|
+
targetRows: number | undefined,
|
|
287
|
+
): { lines: string[]; sessionRows: Array<number | undefined> } {
|
|
179
288
|
const colored = this.#colorReveal(1 + sections.others.length);
|
|
180
289
|
const identity = this.#inkSection(sections.identity, 0, colored);
|
|
181
290
|
let others = sections.others.map((section, index) => ({
|
|
182
291
|
...section,
|
|
183
292
|
lines: this.#inkSection(section.lines, index + 1, colored),
|
|
184
293
|
}));
|
|
185
|
-
const assemble = (): string[] => {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
294
|
+
const assemble = (): { lines: string[]; sessionRows: Array<number | undefined> } => {
|
|
295
|
+
const lines = ["", ...identity];
|
|
296
|
+
const sessionRows: Array<number | undefined> = lines.map(() => undefined);
|
|
297
|
+
for (const section of others) {
|
|
298
|
+
lines.push("", ...section.lines);
|
|
299
|
+
sessionRows.push(undefined, ...(section.sessionRows ?? section.lines.map(() => undefined)));
|
|
300
|
+
}
|
|
301
|
+
return { lines, sessionRows };
|
|
189
302
|
};
|
|
190
|
-
let
|
|
191
|
-
if (targetRows === undefined) return
|
|
192
|
-
while (lines.length > targetRows && others.length > 0) {
|
|
303
|
+
let result = assemble();
|
|
304
|
+
if (targetRows === undefined) return result;
|
|
305
|
+
while (result.lines.length > targetRows && others.length > 0) {
|
|
193
306
|
const drop = others.reduce((worst, section) => (section.dropRank > worst.dropRank ? section : worst));
|
|
194
307
|
others = others.filter(section => section !== drop);
|
|
195
|
-
|
|
308
|
+
result = assemble();
|
|
196
309
|
}
|
|
197
|
-
return lines.slice(0, targetRows);
|
|
310
|
+
return { lines: result.lines.slice(0, targetRows), sessionRows: result.sessionRows.slice(0, targetRows) };
|
|
198
311
|
}
|
|
199
312
|
|
|
200
313
|
#sections(cardWidth: number): { identity: string[]; others: Section[] } {
|
|
201
314
|
const others: Section[] = [];
|
|
202
315
|
const note = this.#releaseNoteLine(cardWidth);
|
|
203
316
|
if (note) others.push({ lines: [note], dropRank: 2 });
|
|
204
|
-
|
|
317
|
+
const sessions = this.#sessionSection(cardWidth);
|
|
318
|
+
others.push({ ...sessions, dropRank: 1 });
|
|
205
319
|
others.push({ lines: [this.#keysLine(cardWidth)], dropRank: 3 });
|
|
206
320
|
return { identity: this.#identityLines(cardWidth), others };
|
|
207
321
|
}
|
|
208
322
|
|
|
209
|
-
// ── Identity:
|
|
323
|
+
// ── Identity: pet + wordmark, then who and where ────────────────────────
|
|
210
324
|
|
|
211
325
|
#identityLines(cardWidth: number): string[] {
|
|
212
|
-
const
|
|
213
|
-
const mark = withMark ? this.#markRows() : [];
|
|
326
|
+
const mark = this.#markRows();
|
|
214
327
|
const markWidth = Math.max(0, ...mark.map(row => visibleWidth(row)));
|
|
215
|
-
const
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
328
|
+
const withMark = cardWidth >= markWidth + MARK_GAP + WORDMARK_WIDTH;
|
|
329
|
+
const withWordmark = this.logoMode !== "ascii" && cardWidth >= WORDMARK_WIDTH;
|
|
330
|
+
const indent = withMark ? markWidth + MARK_GAP : 0;
|
|
331
|
+
const infoWidth = Math.max(1, cardWidth - indent);
|
|
332
|
+
|
|
333
|
+
const head: string[] = withWordmark
|
|
334
|
+
? WORDMARK.map(
|
|
335
|
+
([word, suffix]) =>
|
|
336
|
+
`${theme.bold(theme.fg("accent", word))}${padding(WORDMARK_GAP)}${theme.fg("text", suffix)}`,
|
|
337
|
+
)
|
|
338
|
+
: [this.#truncate(this.#titleLine(!withMark), infoWidth)];
|
|
339
|
+
const versionLine = this.#spread(
|
|
340
|
+
theme.fg("muted", t("welcome.tagline")),
|
|
341
|
+
theme.fg("dim", this.#versionLabel()),
|
|
342
|
+
infoWidth,
|
|
343
|
+
);
|
|
344
|
+
const info = [...head, withWordmark ? versionLine : theme.fg("muted", t("welcome.tagline"))];
|
|
345
|
+
|
|
346
|
+
const rows: string[] = [];
|
|
347
|
+
if (withMark) {
|
|
348
|
+
const count = Math.max(mark.length, info.length);
|
|
349
|
+
for (let row = 0; row < count; row++) {
|
|
350
|
+
const art = mark[row] ?? padding(markWidth);
|
|
351
|
+
const text = info[row] ?? "";
|
|
352
|
+
rows.push(text ? `${art}${padding(MARK_GAP)}${text}` : art);
|
|
353
|
+
}
|
|
354
|
+
} else rows.push(...info);
|
|
355
|
+
// Model and place sit under the wordmark, in the same column.
|
|
356
|
+
for (const line of this.#statusLines(infoWidth)) rows.push(`${padding(indent)}${line}`);
|
|
357
|
+
return rows;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
#versionLabel(): string {
|
|
361
|
+
const buildLabel = this.options.buildLabel ?? formatBuildLabel();
|
|
362
|
+
return `v${this.version} · ${buildLabel}`;
|
|
233
363
|
}
|
|
234
364
|
|
|
235
|
-
/** The Sayknow pet
|
|
365
|
+
/** The Sayknow pet at half size, in half blocks; the pose follows the intro. */
|
|
236
366
|
#markRows(): string[] {
|
|
237
367
|
const step = this.#animStart == null ? -1 : Math.floor((performance.now() - this.#animStart) / WAVE_STEP_MS);
|
|
238
368
|
if (this.logoMode === "ascii") {
|
|
@@ -241,17 +371,30 @@ export class WelcomeComponent implements Component {
|
|
|
241
371
|
return rows.map(row => theme.fg("accent", row));
|
|
242
372
|
}
|
|
243
373
|
const pose = step >= 0 && step < INTRO_POSES.length ? INTRO_POSES[step]! : "base";
|
|
244
|
-
return renderPetHalfBlocks(pose, this.options.petSkin ?? "red", theme.getColorMode());
|
|
374
|
+
return renderPetHalfBlocks(pose, this.options.petSkin ?? "red", theme.getColorMode(), { scale: "half" });
|
|
245
375
|
}
|
|
246
376
|
|
|
247
|
-
/**
|
|
377
|
+
/** One-line title for layouts without room for the wordmark. */
|
|
248
378
|
#titleLine(withIcon = false): string {
|
|
249
|
-
const buildLabel = this.options.buildLabel ?? formatBuildLabel();
|
|
250
379
|
const mark = withIcon && this.logoMode !== "ascii" && theme.icon.pi ? `${theme.icon.pi} ` : "";
|
|
251
|
-
return `${mark}${theme.bold(theme.fg("
|
|
380
|
+
return `${mark}${theme.bold(theme.fg("accent", "Sayknow-CLI"))}${theme.fg("dim", ` ${this.#versionLabel()}`)}`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Model and reasoning, then project path and branch: on one row when both fit whole,
|
|
385
|
+
* otherwise the place gets its own row rather than being cut to nothing.
|
|
386
|
+
*/
|
|
387
|
+
#statusLines(width: number): string[] {
|
|
388
|
+
const model = this.#truncate(this.#modelPart(), width);
|
|
389
|
+
const sep = theme.fg("dim", " · ");
|
|
390
|
+
const fullWhere = this.#wherePart(Number.POSITIVE_INFINITY);
|
|
391
|
+
if (!fullWhere) return [model];
|
|
392
|
+
if (visibleWidth(model) + visibleWidth(sep) + visibleWidth(fullWhere) <= width)
|
|
393
|
+
return [`${model}${sep}${fullWhere}`];
|
|
394
|
+
return [model, this.#wherePart(width)];
|
|
252
395
|
}
|
|
253
396
|
|
|
254
|
-
#
|
|
397
|
+
#modelPart(): string {
|
|
255
398
|
const sep = theme.fg("dim", " · ");
|
|
256
399
|
if (this.modelName === "Unknown" || this.modelName.length === 0) {
|
|
257
400
|
return `${theme.fg("accent", t("welcome.chooseModel"))}${sep}${theme.fg("dim", t("welcome.modelHint"))}`;
|
|
@@ -263,7 +406,7 @@ export class WelcomeComponent implements Component {
|
|
|
263
406
|
return parts.join(sep);
|
|
264
407
|
}
|
|
265
408
|
|
|
266
|
-
#
|
|
409
|
+
#wherePart(width: number): string {
|
|
267
410
|
const { cwd, branch } = this.#snapshot;
|
|
268
411
|
const sep = theme.fg("dim", " · ");
|
|
269
412
|
const branchPart =
|
|
@@ -293,7 +436,19 @@ export class WelcomeComponent implements Component {
|
|
|
293
436
|
|
|
294
437
|
// ── Release note, sessions, keys ────────────────────────────────────────
|
|
295
438
|
|
|
296
|
-
/**
|
|
439
|
+
/**
|
|
440
|
+
* A section header that is also the divider: the label, a hairline rule filling the
|
|
441
|
+
* row, and hints at the right end.
|
|
442
|
+
*/
|
|
443
|
+
#ruleHeader(label: string, hint: string, width: number): string {
|
|
444
|
+
const left = `${label} `;
|
|
445
|
+
const right = hint ? ` ${hint}` : "";
|
|
446
|
+
const fill = width - visibleWidth(left) - visibleWidth(right);
|
|
447
|
+
if (fill < 3) return this.#truncate(`${label}${right}`, width);
|
|
448
|
+
return `${left}${theme.fg("borderMuted", "─".repeat(fill))}${right}`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** One line, only after an update: the new version and its first note, pointing at /changelog. */
|
|
297
452
|
#releaseNoteLine(width: number): string | undefined {
|
|
298
453
|
const changelog = this.options.changelogMarkdown?.trim();
|
|
299
454
|
if (!changelog) return undefined;
|
|
@@ -301,29 +456,46 @@ export class WelcomeComponent implements Component {
|
|
|
301
456
|
const lead = theme.bold(theme.fg("accent", `v${version}`));
|
|
302
457
|
const pointer = theme.fg("dim", "/changelog");
|
|
303
458
|
const first = this.options.collapseChangelog ? undefined : this.#changelogItems(changelog)[0];
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
return this.#spread(`${lead}${body}`, pointer, width);
|
|
459
|
+
const room = width - visibleWidth(lead) - visibleWidth(pointer) - 8;
|
|
460
|
+
const label = first && room >= 12 ? `${lead} ${theme.fg("muted", this.#truncate(first, room))}` : lead;
|
|
461
|
+
return this.#ruleHeader(label, pointer, width);
|
|
308
462
|
}
|
|
309
463
|
|
|
310
|
-
#
|
|
464
|
+
#sessionSection(width: number): Pick<Section, "lines" | "sessionRows"> {
|
|
311
465
|
const heading = theme.bold(theme.fg("accent", t("welcome.sessionTrail")));
|
|
312
|
-
if (this.recentSessions.length === 0)
|
|
466
|
+
if (this.recentSessions.length === 0) {
|
|
467
|
+
return { lines: [this.#ruleHeader(heading, "", width), ` ${theme.fg("dim", t("welcome.noSessions"))}`] };
|
|
468
|
+
}
|
|
313
469
|
const context = this.options.keyDisplayContext ?? { platform: process.platform };
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
470
|
+
const picking = this.#selected !== undefined;
|
|
471
|
+
const hints = picking
|
|
472
|
+
? [t("welcome.pickActive")]
|
|
473
|
+
: [
|
|
474
|
+
...(this.openableCount > 0 ? [t("welcome.pick", { key: "↓" })] : []),
|
|
475
|
+
...(this.options.continueKey
|
|
476
|
+
? [t("welcome.continue", { key: formatKeyHint(this.options.continueKey, context) })]
|
|
477
|
+
: []),
|
|
478
|
+
t("welcome.allSessions", { key: this.#resumeKey() }),
|
|
479
|
+
];
|
|
480
|
+
const lines = [this.#ruleHeader(heading, theme.fg("dim", hints.join(" · ")), width)];
|
|
481
|
+
const sessionRows: Array<number | undefined> = [undefined];
|
|
482
|
+
const openable = this.#openable();
|
|
318
483
|
this.recentSessions.slice(0, SESSION_ROWS).forEach((session, index) => {
|
|
319
|
-
|
|
320
|
-
const
|
|
484
|
+
const openIndex = openable.indexOf(session);
|
|
485
|
+
const selected = picking && openIndex === this.#selected;
|
|
486
|
+
// Idle: the first row is the one the continue key resumes. Picking: the highlighted row.
|
|
487
|
+
const marked = picking ? selected : index === 0 && !!this.options.continueKey;
|
|
488
|
+
const lead = marked ? theme.fg("accent", "› ") : " ";
|
|
321
489
|
const time = theme.fg("dim", session.timeAgo);
|
|
322
490
|
const nameWidth = Math.max(1, width - visibleWidth(lead) - visibleWidth(time) - 2);
|
|
323
|
-
const
|
|
324
|
-
|
|
491
|
+
const tone = selected ? "text" : picking ? "muted" : index === 0 ? "text" : "muted";
|
|
492
|
+
const name = this.#truncate(session.name, nameWidth);
|
|
493
|
+
lines.push(
|
|
494
|
+
this.#spread(`${lead}${selected ? theme.bold(theme.fg(tone, name)) : theme.fg(tone, name)}`, time, width),
|
|
495
|
+
);
|
|
496
|
+
sessionRows.push(openIndex >= 0 ? openIndex : undefined);
|
|
325
497
|
});
|
|
326
|
-
return lines;
|
|
498
|
+
return { lines, sessionRows };
|
|
327
499
|
}
|
|
328
500
|
|
|
329
501
|
#keysLine(width: number): string {
|
|
@@ -335,8 +507,8 @@ export class WelcomeComponent implements Component {
|
|
|
335
507
|
{ key: "?", label: "welcome.keymap" },
|
|
336
508
|
];
|
|
337
509
|
const text = items
|
|
338
|
-
.map(item => `${theme.fg("
|
|
339
|
-
.join(theme.fg("
|
|
510
|
+
.map(item => `${theme.fg("accent", item.key)} ${theme.fg("dim", t(item.label))}`)
|
|
511
|
+
.join(theme.fg("borderMuted", " · "));
|
|
340
512
|
return this.#truncate(text, width);
|
|
341
513
|
}
|
|
342
514
|
|
|
@@ -848,6 +848,8 @@ export class InputController {
|
|
|
848
848
|
|
|
849
849
|
async submitText(text: string, composer: ComposerSubmissionOptions): Promise<void> {
|
|
850
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?.();
|
|
851
853
|
if ((!isSettingsInitialized() || settings.get("emojiAutocomplete")) && text) text = expandEmoticons(text);
|
|
852
854
|
if (this.ctx.hasActiveBtw()) {
|
|
853
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;
|