@sayknow-cli/coding-agent 0.6.4 → 0.6.5
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 +25 -1
- package/README.md +2 -2
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +7 -3
- package/dist/types/i18n/messages/en.d.ts +16 -0
- package/dist/types/modes/components/session-selector.d.ts +7 -3
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- package/dist/types/modes/components/user-message-selector.d.ts +5 -2
- package/dist/types/modes/components/user-message.d.ts +3 -1
- package/dist/types/modes/components/welcome.d.ts +49 -6
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- package/dist/types/modes/theme/defaults/index.d.ts +342 -0
- package/dist/types/modes/theme/theme.d.ts +5 -1
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/session/session-manager.d.ts +9 -0
- package/dist/types/session/session-stars.d.ts +15 -0
- package/dist/types/tui/output-block.d.ts +12 -1
- package/dist/types/tui/utils.d.ts +1 -3
- package/dist/types/utils/session-title-generation.d.ts +6 -0
- package/dist/types/utils/title-generator.d.ts +11 -2
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +8 -0
- package/src/cli/session-picker.ts +4 -0
- package/src/config/keybindings.ts +1 -1
- package/src/config/settings-schema.ts +4 -3
- package/src/i18n/messages/de.messages.ts +4 -1
- package/src/i18n/messages/de.settings.ts +2 -0
- package/src/i18n/messages/de.ts +16 -0
- package/src/i18n/messages/en.ts +16 -0
- package/src/i18n/messages/es.messages.ts +4 -1
- package/src/i18n/messages/es.settings.ts +2 -0
- package/src/i18n/messages/es.ts +16 -0
- package/src/i18n/messages/fr.messages.ts +4 -1
- package/src/i18n/messages/fr.settings.ts +2 -0
- package/src/i18n/messages/fr.ts +16 -0
- package/src/i18n/messages/ja.messages.ts +4 -1
- package/src/i18n/messages/ja.settings.ts +2 -0
- package/src/i18n/messages/ja.ts +16 -0
- package/src/i18n/messages/ko.messages.ts +4 -1
- package/src/i18n/messages/ko.settings.ts +2 -0
- package/src/i18n/messages/ko.ts +16 -0
- package/src/i18n/messages/zh.messages.ts +2 -1
- package/src/i18n/messages/zh.settings.ts +2 -0
- package/src/i18n/messages/zh.ts +16 -0
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/lsp/render.ts +0 -1
- package/src/modes/components/session-selector.ts +71 -21
- package/src/modes/components/sessions-dashboard.ts +8 -5
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/presets.ts +5 -5
- package/src/modes/components/status-line/separators.ts +4 -0
- package/src/modes/components/tool-execution.ts +6 -19
- package/src/modes/components/tool-status-header.ts +103 -12
- package/src/modes/components/user-message-selector.ts +57 -15
- package/src/modes/components/user-message.ts +25 -15
- package/src/modes/components/welcome.ts +374 -478
- package/src/modes/controllers/command-controller.ts +56 -7
- package/src/modes/controllers/input-controller.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +91 -10
- package/src/modes/interactive-mode.ts +101 -12
- package/src/modes/theme/defaults/glow-octopus.json +114 -0
- package/src/modes/theme/defaults/index.ts +6 -0
- package/src/modes/theme/defaults/ink-octopus.json +114 -0
- package/src/modes/theme/defaults/violet-octopus.json +114 -0
- package/src/modes/theme/theme.ts +18 -2
- package/src/modes/types.ts +1 -1
- package/src/prompts/system/title-system.md +1 -1
- package/src/sdk/protocol/operation-inventory.generated.json +40 -0
- package/src/session/agent-session.ts +12 -0
- package/src/session/internal/managed-session-scope.ts +10 -0
- package/src/session/session-manager.ts +26 -1
- package/src/session/session-stars.ts +104 -0
- package/src/slash-commands/builtin-registry.ts +104 -4
- package/src/tools/debug.ts +0 -1
- package/src/tools/fetch.ts +0 -1
- package/src/tui/output-block.ts +29 -61
- package/src/tui/utils.ts +1 -8
- package/src/utils/session-title-generation.ts +29 -0
- package/src/utils/title-generator.ts +46 -2
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import type { ThinkingLevel } from "@sayknow-cli/agent-core";
|
|
2
|
+
import { type Component, padding, truncateToWidth, visibleWidth } from "@sayknow-cli/tui";
|
|
3
3
|
import { formatBuildLabel } from "../../build-metadata";
|
|
4
4
|
import { formatKeyHint, type KeyDisplayContext } from "../../config/keybindings";
|
|
5
|
-
import { t } from "../../i18n";
|
|
6
|
-
import {
|
|
5
|
+
import { type MsgKey, t } from "../../i18n";
|
|
6
|
+
import { theme } from "../../modes/theme/theme";
|
|
7
7
|
|
|
8
8
|
export interface RecentSession {
|
|
9
9
|
name: string;
|
|
@@ -16,6 +16,34 @@ export interface LspServerInfo {
|
|
|
16
16
|
fileTypes: string[];
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export interface WelcomeRoleBinding {
|
|
20
|
+
role: string;
|
|
21
|
+
model: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Workspace and runtime facts shown in the launch ledger. Every field is
|
|
26
|
+
* optional: the ledger renders what is known and fills the rest in as the
|
|
27
|
+
* asynchronous probes (git status, context files, MCP) settle.
|
|
28
|
+
*/
|
|
29
|
+
export interface WelcomeSnapshot {
|
|
30
|
+
/** Display path of the project directory (already home-shortened). */
|
|
31
|
+
cwd?: string;
|
|
32
|
+
/** Current branch, `"detached"`, or `null` outside a git repository. */
|
|
33
|
+
branch?: string | null;
|
|
34
|
+
gitChanges?: { staged: number; unstaged: number; untracked: number } | null;
|
|
35
|
+
/** Latest commits as `<short-sha> <subject>` onelines, newest first. */
|
|
36
|
+
recentCommits?: readonly string[];
|
|
37
|
+
thinkingLevel?: ThinkingLevel | string;
|
|
38
|
+
/** Display name of the active model preset. */
|
|
39
|
+
profile?: string;
|
|
40
|
+
roles?: readonly WelcomeRoleBinding[];
|
|
41
|
+
mcp?: { connected: number; total: number };
|
|
42
|
+
skills?: number;
|
|
43
|
+
/** Display names of the loaded project instruction files (AGENTS.md, …). */
|
|
44
|
+
contextFiles?: readonly string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
19
47
|
export type WelcomeLogoMode = "unicode" | "square" | "ascii";
|
|
20
48
|
export interface WelcomeComponentOptions {
|
|
21
49
|
getViewportRows?: () => number | undefined;
|
|
@@ -26,11 +54,25 @@ export interface WelcomeComponentOptions {
|
|
|
26
54
|
buildLabel?: string;
|
|
27
55
|
keyDisplayContext?: KeyDisplayContext;
|
|
28
56
|
skipLogoAnimation?: boolean;
|
|
57
|
+
snapshot?: WelcomeSnapshot;
|
|
58
|
+
/** Key bound to the resume picker (`app.session.resume`); the sessions heading names it. */
|
|
59
|
+
resumeKey?: string;
|
|
29
60
|
}
|
|
30
61
|
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
const
|
|
62
|
+
/** Below this width the ledger and the activity column stack instead of sitting side by side. */
|
|
63
|
+
const TWO_COLUMN_MIN_WIDTH = 100;
|
|
64
|
+
const COLUMN_GAP = 4;
|
|
65
|
+
const MIN_RIGHT_COLUMN = 36;
|
|
66
|
+
const WHATS_NEW_ROWS = 3;
|
|
67
|
+
/** Recent sessions on the launch screen; the resume picker has the rest. */
|
|
68
|
+
const SESSION_ROWS = 3;
|
|
69
|
+
const MAX_LSP_ROWS = 3;
|
|
70
|
+
const MAX_COMMIT_ROWS = 3;
|
|
71
|
+
|
|
72
|
+
/** Stagger between two ledger sections appearing during the launch reveal. */
|
|
73
|
+
const SECTION_STAGGER_MS = 55;
|
|
74
|
+
/** How long the whole ledger stays in dim ink before the first section takes its colors. */
|
|
75
|
+
const SECTION_SETTLE_MS = 90;
|
|
34
76
|
|
|
35
77
|
function flowKeyItems(context: KeyDisplayContext): ReadonlyArray<{ key: string; label: string }> {
|
|
36
78
|
const newlineKey = context.platform === "win32" ? "alt+enter" : "ctrl+j";
|
|
@@ -48,14 +90,30 @@ function flowKeyItems(context: KeyDisplayContext): ReadonlyArray<{ key: string;
|
|
|
48
90
|
];
|
|
49
91
|
}
|
|
50
92
|
|
|
93
|
+
const WORKFLOWS: ReadonlyArray<{ command: string; key: MsgKey }> = [
|
|
94
|
+
{ command: "/deep-interview", key: "welcome.wf.deepInterview" },
|
|
95
|
+
{ command: "/ralplan", key: "welcome.wf.ralplan" },
|
|
96
|
+
{ command: "/ultragoal", key: "welcome.wf.ultragoal" },
|
|
97
|
+
{ command: "/team", key: "welcome.wf.team" },
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
/** A block of rows revealed together during the launch intro. */
|
|
101
|
+
interface Section {
|
|
102
|
+
lines: string[];
|
|
103
|
+
}
|
|
104
|
+
|
|
51
105
|
/**
|
|
52
|
-
* Sayknow-CLI launch surface:
|
|
53
|
-
*
|
|
54
|
-
*
|
|
106
|
+
* Sayknow-CLI launch surface: an open, borderless ledger. The left column is
|
|
107
|
+
* the state of this workspace — path, branch, model, reasoning, preset, role
|
|
108
|
+
* agents, tooling — so the first screen answers "what am I about to run with".
|
|
109
|
+
* The right column carries activity: what changed, recent sessions, workflows
|
|
110
|
+
* and keys. No enclosing box and no hero wordmark: the octopus mark and the
|
|
111
|
+
* facts carry the identity.
|
|
55
112
|
*/
|
|
56
113
|
export class WelcomeComponent implements Component {
|
|
57
114
|
#animStart: number | null = null;
|
|
58
115
|
#animTimer: NodeJS.Timeout | null = null;
|
|
116
|
+
#snapshot: WelcomeSnapshot;
|
|
59
117
|
|
|
60
118
|
constructor(
|
|
61
119
|
private readonly version: string,
|
|
@@ -65,14 +123,17 @@ export class WelcomeComponent implements Component {
|
|
|
65
123
|
private lspServers: LspServerInfo[] = [],
|
|
66
124
|
private readonly logoMode: WelcomeLogoMode = "unicode",
|
|
67
125
|
private readonly options: WelcomeComponentOptions = {},
|
|
68
|
-
) {
|
|
126
|
+
) {
|
|
127
|
+
this.#snapshot = { ...options.snapshot };
|
|
128
|
+
}
|
|
69
129
|
|
|
70
130
|
invalidate(): void {}
|
|
71
131
|
|
|
72
132
|
/**
|
|
73
|
-
* Play a one-shot
|
|
74
|
-
*
|
|
75
|
-
*
|
|
133
|
+
* Play a short one-shot reveal: sections appear top to bottom a few frames
|
|
134
|
+
* apart, each settling from dim into its colors. Launch happens once per
|
|
135
|
+
* session, so a sub-second reveal is affordable; it never blocks input.
|
|
136
|
+
* Safe to call multiple times — subsequent calls reset and replay.
|
|
76
137
|
*/
|
|
77
138
|
playIntro(requestRender: () => void): void {
|
|
78
139
|
this.#stopAnimation();
|
|
@@ -117,261 +178,239 @@ export class WelcomeComponent implements Component {
|
|
|
117
178
|
this.lspServers = servers;
|
|
118
179
|
}
|
|
119
180
|
|
|
181
|
+
/** Merge newly probed workspace facts into the ledger. */
|
|
182
|
+
setSnapshot(patch: WelcomeSnapshot): void {
|
|
183
|
+
this.#snapshot = { ...this.#snapshot, ...patch };
|
|
184
|
+
}
|
|
185
|
+
|
|
120
186
|
render(termWidth: number): string[] {
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
if (
|
|
124
|
-
return [];
|
|
125
|
-
}
|
|
187
|
+
const gutterWidth = this.#rightGutterWidth(termWidth);
|
|
188
|
+
const width = Math.max(0, termWidth - gutterWidth);
|
|
189
|
+
if (width < 4) return [];
|
|
126
190
|
|
|
127
191
|
const targetRows = this.#targetRows(termWidth);
|
|
128
|
-
if (targetRows !== undefined && targetRows <= 0)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
);
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const logoColored = this.#currentLogoFrame(logoLines);
|
|
159
|
-
|
|
160
|
-
// When no model is resolved yet, guide the user instead of showing "Unknown".
|
|
161
|
-
const hasModel = this.modelName !== "Unknown" && this.modelName.length > 0;
|
|
162
|
-
const identityTail = hasModel
|
|
163
|
-
? [this.#centerText(modelPill, leftCol), this.#centerText(providerPill, leftCol)]
|
|
164
|
-
: [
|
|
165
|
-
this.#centerText(
|
|
166
|
-
this.#pill(theme.icon.model || "model", t("welcome.chooseModel"), "statusLineModel"),
|
|
167
|
-
leftCol,
|
|
168
|
-
),
|
|
169
|
-
this.#centerText(theme.fg("dim", t("welcome.modelHint")), leftCol),
|
|
170
|
-
];
|
|
171
|
-
|
|
172
|
-
// Left column - centered identity stack: wordmark, tagline, then model state.
|
|
173
|
-
const leftLines = [
|
|
174
|
-
"",
|
|
175
|
-
...logoColored.map(l => this.#centerText(l, leftCol)),
|
|
176
|
-
"",
|
|
177
|
-
this.#centerText(theme.fg("muted", t("welcome.tagline")), leftCol),
|
|
178
|
-
"",
|
|
179
|
-
...identityTail,
|
|
180
|
-
];
|
|
181
|
-
|
|
182
|
-
const buildSeparator = (columnWidth: number): string =>
|
|
183
|
-
` ${theme.fg("dim", theme.boxRound.horizontal.repeat(Math.max(0, columnWidth - 2)))}`;
|
|
184
|
-
|
|
185
|
-
const rightColumnWidth = showRightColumn ? rightCol : leftCol;
|
|
186
|
-
const separator = buildSeparator(rightColumnWidth);
|
|
187
|
-
const lspLines: string[] = [];
|
|
188
|
-
if (this.lspServers.length === 0) {
|
|
189
|
-
lspLines.push(` ${theme.fg("dim", t("welcome.noLsp"))}`);
|
|
190
|
-
} else {
|
|
191
|
-
for (const server of this.lspServers.slice(0, 4)) {
|
|
192
|
-
const icon =
|
|
193
|
-
server.status === "ready"
|
|
194
|
-
? theme.styledSymbol("status.success", "success")
|
|
195
|
-
: server.status === "error"
|
|
196
|
-
? theme.styledSymbol("status.error", "error")
|
|
197
|
-
: theme.styledSymbol("status.pending", "muted");
|
|
198
|
-
const exts = server.fileTypes.slice(0, 3).join(" ");
|
|
199
|
-
lspLines.push(` ${icon} ${theme.fg("muted", server.name)} ${theme.fg("dim", exts)}`);
|
|
192
|
+
if (targetRows !== undefined && targetRows <= 0) return [];
|
|
193
|
+
|
|
194
|
+
const header = this.#fitToWidth(this.#headerLine(width), width);
|
|
195
|
+
if (targetRows === 1) return this.#withRightGutter([header], gutterWidth);
|
|
196
|
+
|
|
197
|
+
const rule = theme.fg("borderMuted", this.#ruleGlyph().repeat(width));
|
|
198
|
+
const bodyRows = targetRows === undefined ? undefined : Math.max(0, targetRows - 2);
|
|
199
|
+
|
|
200
|
+
const twoColumn = width >= TWO_COLUMN_MIN_WIDTH;
|
|
201
|
+
const leftWidth = twoColumn
|
|
202
|
+
? Math.min(Math.max(44, Math.floor(width * 0.52)), width - COLUMN_GAP - MIN_RIGHT_COLUMN)
|
|
203
|
+
: width;
|
|
204
|
+
const rightWidth = twoColumn ? width - leftWidth - COLUMN_GAP : width;
|
|
205
|
+
|
|
206
|
+
const ledger = this.#ledgerSections(leftWidth);
|
|
207
|
+
const activity = this.#activitySections(rightWidth);
|
|
208
|
+
|
|
209
|
+
const reveal = this.#revealState(ledger.length + activity.length);
|
|
210
|
+
const leftLines = this.#revealLines(ledger, 0, reveal);
|
|
211
|
+
const rightLines = this.#revealLines(activity, ledger.length, reveal);
|
|
212
|
+
|
|
213
|
+
const body: string[] = [];
|
|
214
|
+
if (twoColumn) {
|
|
215
|
+
const rows = bodyRows ?? Math.max(leftLines.length, rightLines.length);
|
|
216
|
+
const left = this.#clip(leftLines, rows);
|
|
217
|
+
const right = this.#clip(rightLines, rows);
|
|
218
|
+
const gap = padding(COLUMN_GAP);
|
|
219
|
+
for (let i = 0; i < rows; i++) {
|
|
220
|
+
body.push(this.#fitToWidth(left[i] ?? "", leftWidth) + gap + this.#fitToWidth(right[i] ?? "", rightWidth));
|
|
200
221
|
}
|
|
222
|
+
} else {
|
|
223
|
+
const stacked = [...leftLines, "", ...rightLines];
|
|
224
|
+
const rows = bodyRows ?? stacked.length;
|
|
225
|
+
for (const line of this.#clip(stacked, rows)) body.push(this.#fitToWidth(line, width));
|
|
226
|
+
while (body.length < rows) body.push(padding(width));
|
|
201
227
|
}
|
|
202
228
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const changelogLines = this.#whatsNewLines(rightColumnWidth, changelogRowLimit);
|
|
206
|
-
const flowRowLimit = this.#flowKeyRowLimit(
|
|
207
|
-
targetContentRows,
|
|
208
|
-
changelogLines.length,
|
|
209
|
-
lspLines.length,
|
|
210
|
-
rightColumnWidth,
|
|
211
|
-
);
|
|
212
|
-
const flowLines = this.#flowKeyLines(rightColumnWidth, flowRowLimit);
|
|
213
|
-
const sessionLimit = this.#sessionTrailLimit(
|
|
214
|
-
targetContentRows,
|
|
215
|
-
changelogLines.length,
|
|
216
|
-
lspLines.length,
|
|
217
|
-
flowLines.length,
|
|
218
|
-
);
|
|
219
|
-
const sessionLines = this.#sessionTrailLines(rightColumnWidth, sessionLimit);
|
|
229
|
+
return this.#withRightGutter([header, rule, ...body], gutterWidth);
|
|
230
|
+
}
|
|
220
231
|
|
|
221
|
-
|
|
222
|
-
const wf = (cmd: string, desc: string): string => {
|
|
223
|
-
const pad = " ".repeat(Math.max(1, 17 - visibleWidth(cmd)));
|
|
224
|
-
return ` ${theme.fg("accent", cmd)}${pad}${theme.fg("muted", desc)}`;
|
|
225
|
-
};
|
|
232
|
+
// ── Header ──────────────────────────────────────────────────────────────
|
|
226
233
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
wf("/ultragoal", t("welcome.wf.ultragoal")),
|
|
236
|
-
wf("/team", t("welcome.wf.team")),
|
|
237
|
-
separator,
|
|
238
|
-
` ${theme.bold(theme.fg("accent", t("welcome.flowKeys")))}`,
|
|
239
|
-
...flowLines,
|
|
240
|
-
separator,
|
|
241
|
-
` ${theme.bold(theme.fg("accent", t("welcome.projectPulse")))}`,
|
|
242
|
-
...lspLines,
|
|
243
|
-
separator,
|
|
244
|
-
` ${theme.bold(theme.fg("accent", t("welcome.sessionTrail")))}`,
|
|
245
|
-
...sessionLines,
|
|
246
|
-
"",
|
|
247
|
-
];
|
|
234
|
+
#headerLine(width: number): string {
|
|
235
|
+
const buildLabel = this.options.buildLabel ?? formatBuildLabel();
|
|
236
|
+
const mark = theme.icon.pi ? `${theme.icon.pi} ` : "";
|
|
237
|
+
const left = ` ${mark}${theme.bold(theme.fg("text", "Sayknow-CLI"))}${theme.fg("dim", ` v${this.version} · ${buildLabel}`)}`;
|
|
238
|
+
const tagline = theme.fg("muted", t("welcome.tagline"));
|
|
239
|
+
const room = width - visibleWidth(left) - visibleWidth(tagline) - 1;
|
|
240
|
+
return room >= 2 ? `${left}${padding(room)}${tagline} ` : left;
|
|
241
|
+
}
|
|
248
242
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const outputRows = targetRows === undefined ? Math.max(3, contentRows + 2) : targetRows;
|
|
253
|
-
const bodyRows = Math.max(0, outputRows - 2);
|
|
243
|
+
#ruleGlyph(): string {
|
|
244
|
+
return this.logoMode === "ascii" ? "-" : "─";
|
|
245
|
+
}
|
|
254
246
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
247
|
+
// ── Left column: workspace ledger ───────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
#ledgerSections(width: number): Section[] {
|
|
250
|
+
const labels = {
|
|
251
|
+
workspace: t("welcome.label.workspace"),
|
|
252
|
+
branch: t("welcome.label.branch"),
|
|
253
|
+
commits: t("welcome.label.commits"),
|
|
254
|
+
model: t("welcome.label.model"),
|
|
255
|
+
reasoning: t("welcome.label.reasoning"),
|
|
256
|
+
preset: t("welcome.label.preset"),
|
|
257
|
+
roles: t("welcome.label.roles"),
|
|
258
|
+
tools: t("welcome.label.tools"),
|
|
259
|
+
};
|
|
260
|
+
const labelWidth = Math.max(...Object.values(labels).map(label => visibleWidth(label))) + 2;
|
|
261
|
+
const valueWidth = Math.max(1, width - labelWidth - 1);
|
|
262
|
+
const row = (label: string, value: string): string =>
|
|
263
|
+
` ${theme.fg("dim", label)}${padding(Math.max(0, labelWidth - visibleWidth(label)))}${this.#truncate(value, valueWidth)}`;
|
|
264
|
+
const continuation = (value: string): string => ` ${padding(labelWidth)}${this.#truncate(value, valueWidth)}`;
|
|
265
|
+
const snapshot = this.#snapshot;
|
|
266
|
+
const sep = theme.fg("dim", " · ");
|
|
267
|
+
|
|
268
|
+
// Where
|
|
269
|
+
const where: string[] = [];
|
|
270
|
+
if (snapshot.cwd)
|
|
271
|
+
where.push(row(labels.workspace, theme.fg("statusLinePath", this.#shortenFromLeft(snapshot.cwd, valueWidth))));
|
|
272
|
+
if (snapshot.branch !== undefined) {
|
|
273
|
+
const branch =
|
|
274
|
+
snapshot.branch === null
|
|
275
|
+
? theme.fg("dim", t("welcome.noGit"))
|
|
276
|
+
: `${theme.fg(this.#isDirty() ? "statusLineGitDirty" : "statusLineGitClean", snapshot.branch)}${this.#gitChangeSummary(sep)}`;
|
|
277
|
+
where.push(row(labels.branch, branch));
|
|
278
|
+
}
|
|
279
|
+
(snapshot.recentCommits ?? []).slice(0, MAX_COMMIT_ROWS).forEach((oneline, index) => {
|
|
280
|
+
const space = oneline.indexOf(" ");
|
|
281
|
+
const value =
|
|
282
|
+
space > 0
|
|
283
|
+
? `${theme.fg("accent", oneline.slice(0, space))} ${theme.fg("muted", oneline.slice(space + 1))}`
|
|
284
|
+
: theme.fg("muted", oneline);
|
|
285
|
+
where.push(index === 0 ? row(labels.commits, value) : continuation(value));
|
|
286
|
+
});
|
|
262
287
|
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
288
|
+
// Brain
|
|
289
|
+
const brain: string[] = [];
|
|
290
|
+
const hasModel = this.modelName !== "Unknown" && this.modelName.length > 0;
|
|
291
|
+
brain.push(
|
|
292
|
+
row(
|
|
293
|
+
labels.model,
|
|
294
|
+
hasModel
|
|
295
|
+
? `${theme.bold(theme.fg("statusLineModel", this.modelName))}${sep}${theme.fg("muted", this.providerName)}`
|
|
296
|
+
: `${theme.fg("accent", t("welcome.chooseModel"))}${sep}${theme.fg("dim", t("welcome.modelHint"))}`,
|
|
297
|
+
),
|
|
298
|
+
);
|
|
299
|
+
if (snapshot.thinkingLevel) {
|
|
300
|
+
const level = String(snapshot.thinkingLevel);
|
|
301
|
+
brain.push(row(labels.reasoning, theme.getThinkingBorderColor(level as ThinkingLevel)(level)));
|
|
275
302
|
}
|
|
276
|
-
|
|
277
|
-
|
|
303
|
+
brain.push(row(labels.preset, snapshot.profile ? theme.fg("text", snapshot.profile) : theme.fg("dim", "—")));
|
|
304
|
+
const roles = snapshot.roles ?? [];
|
|
305
|
+
if (roles.length === 0) {
|
|
306
|
+
brain.push(row(labels.roles, theme.fg("dim", t("welcome.rolesInherit"))));
|
|
307
|
+
} else {
|
|
308
|
+
const roleWidth = Math.max(...roles.map(role => visibleWidth(role.role))) + 2;
|
|
309
|
+
roles.forEach((binding, index) => {
|
|
310
|
+
const value = `${theme.fg("muted", binding.role)}${padding(roleWidth - visibleWidth(binding.role))}${theme.fg("text", binding.model)}`;
|
|
311
|
+
brain.push(index === 0 ? row(labels.roles, value) : continuation(value));
|
|
312
|
+
});
|
|
278
313
|
}
|
|
279
314
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
]
|
|
297
|
-
: leftLines;
|
|
298
|
-
const singleBlock = this.#fitBlock([...compactLeftLines, separator, ...rightLines], bodyRows, "top");
|
|
299
|
-
for (let i = 0; i < bodyRows; i++) {
|
|
300
|
-
lines.push(v + this.#fitToWidth(singleBlock[i] ?? "", leftCol) + v);
|
|
301
|
-
}
|
|
302
|
-
lines.push(bl + h.repeat(leftCol) + br);
|
|
315
|
+
// Hands
|
|
316
|
+
const hands: string[] = [];
|
|
317
|
+
const toolFacts: string[] = [];
|
|
318
|
+
if (snapshot.mcp) {
|
|
319
|
+
const { connected, total } = snapshot.mcp;
|
|
320
|
+
const color = total === 0 ? "dim" : connected === total ? "success" : "warning";
|
|
321
|
+
toolFacts.push(`${theme.fg("muted", "MCP")} ${theme.fg(color, total === 0 ? "0" : `${connected}/${total}`)}`);
|
|
322
|
+
}
|
|
323
|
+
if (snapshot.skills !== undefined) {
|
|
324
|
+
toolFacts.push(`${theme.fg("muted", t("welcome.skills"))} ${theme.fg("text", String(snapshot.skills))}`);
|
|
325
|
+
}
|
|
326
|
+
if (snapshot.contextFiles !== undefined) {
|
|
327
|
+
const files = snapshot.contextFiles;
|
|
328
|
+
const shown = files.length === 0 ? theme.fg("dim", "—") : theme.fg("text", files[0]!);
|
|
329
|
+
const more = files.length > 1 ? theme.fg("dim", ` +${files.length - 1}`) : "";
|
|
330
|
+
toolFacts.push(`${theme.fg("muted", t("welcome.rules"))} ${shown}${more}`);
|
|
303
331
|
}
|
|
332
|
+
const lspLines = this.#lspLines();
|
|
333
|
+
const toolRows = [...(toolFacts.length > 0 ? [toolFacts.join(sep)] : []), ...lspLines];
|
|
334
|
+
toolRows.forEach((value, index) => {
|
|
335
|
+
hands.push(index === 0 ? row(labels.tools, value) : continuation(value));
|
|
336
|
+
});
|
|
304
337
|
|
|
305
|
-
return
|
|
338
|
+
return [where, brain, hands].filter(lines => lines.length > 0).map(lines => ({ lines: [...lines, ""] }));
|
|
306
339
|
}
|
|
307
340
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
341
|
+
#lspLines(): string[] {
|
|
342
|
+
if (this.lspServers.length === 0) return [theme.fg("dim", t("welcome.noLsp"))];
|
|
343
|
+
const lines = this.lspServers.slice(0, MAX_LSP_ROWS).map(server => {
|
|
344
|
+
const icon =
|
|
345
|
+
server.status === "ready"
|
|
346
|
+
? theme.styledSymbol("status.success", "success")
|
|
347
|
+
: server.status === "error"
|
|
348
|
+
? theme.styledSymbol("status.error", "error")
|
|
349
|
+
: theme.styledSymbol("status.pending", "muted");
|
|
350
|
+
return `${icon} ${theme.fg("muted", server.name)} ${theme.fg("dim", server.fileTypes.slice(0, 3).join(" "))}`;
|
|
351
|
+
});
|
|
352
|
+
const hidden = this.lspServers.length - MAX_LSP_ROWS;
|
|
353
|
+
if (hidden > 0) lines.push(theme.fg("dim", `+${hidden} LSP`));
|
|
354
|
+
return lines;
|
|
317
355
|
}
|
|
318
356
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (visLen > width) {
|
|
323
|
-
return truncateToWidth(str, width, null, true);
|
|
324
|
-
}
|
|
325
|
-
return str + padding(width - visLen);
|
|
326
|
-
}
|
|
327
|
-
#rightGutterWidth(termWidth: number): number {
|
|
328
|
-
const configured = this.options.rightGutterWidth ?? 0;
|
|
329
|
-
if (!Number.isFinite(configured) || configured <= 0) return 0;
|
|
330
|
-
const gutterWidth = Math.floor(configured);
|
|
331
|
-
return Math.min(gutterWidth, Math.max(0, termWidth - 4));
|
|
357
|
+
#isDirty(): boolean {
|
|
358
|
+
const changes = this.#snapshot.gitChanges;
|
|
359
|
+
return !!changes && changes.staged + changes.unstaged + changes.untracked > 0;
|
|
332
360
|
}
|
|
333
361
|
|
|
334
|
-
#
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
362
|
+
#gitChangeSummary(sep: string): string {
|
|
363
|
+
const changes = this.#snapshot.gitChanges;
|
|
364
|
+
if (changes === undefined) return "";
|
|
365
|
+
if (changes === null) return "";
|
|
366
|
+
if (!this.#isDirty()) return `${sep}${theme.fg("dim", t("welcome.clean"))}`;
|
|
367
|
+
const parts: string[] = [];
|
|
368
|
+
if (changes.staged > 0) parts.push(theme.fg("statusLineStaged", `+${changes.staged}`));
|
|
369
|
+
if (changes.unstaged > 0) parts.push(theme.fg("statusLineDirty", `~${changes.unstaged}`));
|
|
370
|
+
if (changes.untracked > 0) parts.push(theme.fg("statusLineUntracked", `?${changes.untracked}`));
|
|
371
|
+
return `${sep}${parts.join(" ")}`;
|
|
338
372
|
}
|
|
339
373
|
|
|
340
|
-
|
|
341
|
-
const viewportRows = this.options.getViewportRows?.();
|
|
342
|
-
if (typeof viewportRows !== "number" || !Number.isFinite(viewportRows) || viewportRows <= 0) {
|
|
343
|
-
return undefined;
|
|
344
|
-
}
|
|
345
|
-
const reservedRows = Math.max(0, Math.floor(this.options.getReservedBottomRows?.(termWidth) ?? 0));
|
|
346
|
-
return Math.max(0, Math.floor(viewportRows) - reservedRows);
|
|
347
|
-
}
|
|
374
|
+
// ── Right column: activity ──────────────────────────────────────────────
|
|
348
375
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
376
|
+
/**
|
|
377
|
+
* The launch screen stays short on purpose: three recent sessions and three
|
|
378
|
+
* notes, never more. The full session list lives in the resume picker, and the
|
|
379
|
+
* sessions heading names the key that opens it.
|
|
380
|
+
*/
|
|
381
|
+
#activitySections(width: number): Section[] {
|
|
382
|
+
const keyRows = this.#flowKeyRows(width);
|
|
383
|
+
const heading = (label: string, note?: string): string =>
|
|
384
|
+
` ${theme.bold(theme.fg("accent", label))}${note ? theme.fg("dim", ` ${note}`) : ""}`;
|
|
385
|
+
const whatsNew = this.#whatsNewLines(width, WHATS_NEW_ROWS);
|
|
356
386
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
387
|
+
const changelog = this.options.changelogMarkdown?.trim();
|
|
388
|
+
const version = changelog ? this.#latestChangelogVersion(changelog) : undefined;
|
|
389
|
+
const context = this.options.keyDisplayContext ?? { platform: process.platform };
|
|
390
|
+
const resumeKey = this.options.resumeKey ? formatKeyHint(this.options.resumeKey, context) : "/resume";
|
|
391
|
+
const sessionsNote = this.recentSessions.length > 0 ? t("welcome.allSessions", { key: resumeKey }) : undefined;
|
|
392
|
+
|
|
393
|
+
const workflowWidth = Math.max(...WORKFLOWS.map(item => visibleWidth(item.command))) + 2;
|
|
394
|
+
return [
|
|
395
|
+
{ lines: [heading(t("welcome.sessionTrail"), sessionsNote), ...this.#sessionTrailLines(width), ""] },
|
|
396
|
+
{ lines: [heading(t("welcome.whatsNew"), version ? `v${version}` : undefined), ...whatsNew, ""] },
|
|
397
|
+
{
|
|
398
|
+
lines: [
|
|
399
|
+
heading(t("welcome.workflows")),
|
|
400
|
+
...WORKFLOWS.map(
|
|
401
|
+
item =>
|
|
402
|
+
` ${theme.fg("accent", item.command)}${padding(workflowWidth - visibleWidth(item.command))}${theme.fg("muted", t(item.key))}`,
|
|
403
|
+
),
|
|
404
|
+
"",
|
|
405
|
+
],
|
|
406
|
+
},
|
|
407
|
+
{ lines: [heading(t("welcome.flowKeys")), ...keyRows] },
|
|
408
|
+
];
|
|
370
409
|
}
|
|
371
410
|
|
|
372
411
|
#flowKeyItemText(item: { key: string; label: string }): string {
|
|
373
412
|
const context = this.options.keyDisplayContext ?? { platform: process.platform };
|
|
374
|
-
return `${theme.fg("
|
|
413
|
+
return `${theme.fg("text", formatKeyHint(item.key, context))}${theme.fg("dim", ` ${this.#flowKeyLabel(item.label)}`)}`;
|
|
375
414
|
}
|
|
376
415
|
|
|
377
416
|
#flowKeyLabel(label: string): string {
|
|
@@ -396,107 +435,35 @@ export class WelcomeComponent implements Component {
|
|
|
396
435
|
}
|
|
397
436
|
|
|
398
437
|
#flowKeyRows(width: number): string[] {
|
|
399
|
-
const contentWidth = Math.max(1, width -
|
|
400
|
-
const separator =
|
|
438
|
+
const contentWidth = Math.max(1, width - 2);
|
|
439
|
+
const separator = theme.fg("dim", " ");
|
|
401
440
|
const rows: string[] = [];
|
|
402
441
|
let current = "";
|
|
403
442
|
for (const item of flowKeyItems(this.options.keyDisplayContext ?? { platform: process.platform })) {
|
|
404
443
|
const segment = this.#flowKeyItemText(item);
|
|
405
444
|
const next = current ? `${current}${separator}${segment}` : segment;
|
|
406
445
|
if (current && visibleWidth(next) > contentWidth) {
|
|
407
|
-
rows.push(`
|
|
446
|
+
rows.push(` ${current}`);
|
|
408
447
|
current = segment;
|
|
409
448
|
} else {
|
|
410
449
|
current = next;
|
|
411
450
|
}
|
|
412
451
|
}
|
|
413
|
-
if (current) rows.push(`
|
|
414
|
-
return rows
|
|
452
|
+
if (current) rows.push(` ${current}`);
|
|
453
|
+
return rows;
|
|
415
454
|
}
|
|
416
455
|
|
|
417
|
-
#
|
|
418
|
-
const rows = this.#flowKeyRows(width);
|
|
419
|
-
const rowLimit = Math.max(1, Math.floor(maxRows));
|
|
420
|
-
if (rows.length <= rowLimit) return rows;
|
|
421
|
-
if (rowLimit === 1) {
|
|
422
|
-
const firstItem = flowKeyItems(this.options.keyDisplayContext ?? { platform: process.platform })[0];
|
|
423
|
-
const firstSegment = firstItem ? this.#flowKeyItemText(firstItem) : theme.fg("dim", "keys");
|
|
424
|
-
return [this.#fitToWidth(` ${firstSegment} ${theme.fg("dim", "· … ")}${theme.bold("/help")}`, width)];
|
|
425
|
-
}
|
|
426
|
-
return [...rows.slice(0, rowLimit - 1), ` ${theme.fg("dim", `… ${theme.bold("/help")} for more`)}`];
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
#flowKeyRowLimit(
|
|
430
|
-
targetContentRows: number | undefined,
|
|
431
|
-
changelogLineCount: number,
|
|
432
|
-
lspLineCount: number,
|
|
433
|
-
rightColumnWidth: number,
|
|
434
|
-
): number {
|
|
435
|
-
const preferredRows = this.#flowKeyRows(rightColumnWidth).length;
|
|
436
|
-
if (targetContentRows === undefined) return preferredRows;
|
|
437
|
-
|
|
438
|
-
const sessionBaselineRows = this.recentSessions.length === 0 ? 1 : Math.min(3, this.recentSessions.length);
|
|
439
|
-
const availableRows =
|
|
440
|
-
targetContentRows -
|
|
441
|
-
WELCOME_STATIC_RIGHT_ROWS_EXCLUDING_DYNAMIC_SECTIONS -
|
|
442
|
-
changelogLineCount -
|
|
443
|
-
lspLineCount -
|
|
444
|
-
sessionBaselineRows;
|
|
445
|
-
return Math.max(1, Math.min(preferredRows, availableRows));
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
#whatsNewRowLimit(targetContentRows: number | undefined, lspLineCount: number, flowLineCount: number): number {
|
|
449
|
-
if (targetContentRows === undefined) return 5;
|
|
450
|
-
|
|
451
|
-
const sessionBaselineRows = this.recentSessions.length === 0 ? 1 : Math.min(3, this.recentSessions.length);
|
|
452
|
-
const dynamicRows = Math.max(
|
|
453
|
-
1,
|
|
454
|
-
targetContentRows - WELCOME_STATIC_RIGHT_ROWS_EXCLUDING_DYNAMIC_SECTIONS - flowLineCount - lspLineCount,
|
|
455
|
-
);
|
|
456
|
-
const rowsAfterBaselineSessions = Math.max(1, dynamicRows - sessionBaselineRows);
|
|
457
|
-
const spareRows = Math.max(0, rowsAfterBaselineSessions - DEFAULT_WHATS_NEW_ROWS);
|
|
458
|
-
return Math.max(
|
|
459
|
-
1,
|
|
460
|
-
Math.min(MAX_WHATS_NEW_ROWS, rowsAfterBaselineSessions, DEFAULT_WHATS_NEW_ROWS + Math.floor(spareRows / 2)),
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
#sessionTrailLimit(
|
|
465
|
-
targetContentRows: number | undefined,
|
|
466
|
-
changelogLineCount: number,
|
|
467
|
-
lspLineCount: number,
|
|
468
|
-
flowLineCount: number,
|
|
469
|
-
): number {
|
|
470
|
-
if (this.recentSessions.length === 0) return 0;
|
|
471
|
-
|
|
472
|
-
const defaultLimit = Math.min(3, this.recentSessions.length);
|
|
473
|
-
if (targetContentRows === undefined) return defaultLimit;
|
|
474
|
-
|
|
475
|
-
const rowsWithDefaultTrail =
|
|
476
|
-
WELCOME_STATIC_RIGHT_ROWS_EXCLUDING_DYNAMIC_SECTIONS +
|
|
477
|
-
changelogLineCount +
|
|
478
|
-
flowLineCount +
|
|
479
|
-
lspLineCount +
|
|
480
|
-
defaultLimit;
|
|
481
|
-
const extraRows = Math.max(0, targetContentRows - rowsWithDefaultTrail);
|
|
482
|
-
return Math.min(this.recentSessions.length, defaultLimit + extraRows);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
#sessionTrailLines(rightColumnWidth: number, limit: number): string[] {
|
|
456
|
+
#sessionTrailLines(width: number): string[] {
|
|
486
457
|
if (this.recentSessions.length === 0) {
|
|
487
|
-
return [`
|
|
458
|
+
return [` ${theme.fg("dim", t("welcome.noSessions"))}`];
|
|
488
459
|
}
|
|
489
|
-
|
|
490
|
-
const bulletPrefix = ` ${theme.md.bullet} `;
|
|
491
|
-
const prefixWidth = visibleWidth(bulletPrefix);
|
|
492
460
|
const lines: string[] = [];
|
|
493
|
-
for (const session of this.recentSessions.slice(0,
|
|
494
|
-
const
|
|
495
|
-
const
|
|
496
|
-
const
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
lines.push(`${theme.fg("dim", bulletPrefix)}${theme.fg("muted", name)}${theme.fg("dim", timeSuffixRaw)}`);
|
|
461
|
+
for (const session of this.recentSessions.slice(0, SESSION_ROWS)) {
|
|
462
|
+
const time = theme.fg("dim", session.timeAgo);
|
|
463
|
+
const nameBudget = Math.max(1, width - 2 - visibleWidth(session.timeAgo) - 2);
|
|
464
|
+
const name = this.#truncate(session.name, nameBudget);
|
|
465
|
+
const pad = padding(Math.max(1, width - 2 - visibleWidth(name) - visibleWidth(session.timeAgo)));
|
|
466
|
+
lines.push(` ${theme.fg("muted", name)}${pad}${time}`);
|
|
500
467
|
}
|
|
501
468
|
return lines;
|
|
502
469
|
}
|
|
@@ -504,37 +471,27 @@ export class WelcomeComponent implements Component {
|
|
|
504
471
|
#whatsNewLines(width: number, maxRows: number): string[] {
|
|
505
472
|
const rowLimit = Math.max(1, Math.floor(maxRows));
|
|
506
473
|
const changelog = this.options.changelogMarkdown?.trim();
|
|
507
|
-
if (!changelog) {
|
|
508
|
-
return [` ${theme.fg("dim", "Ready for your next prompt")}`];
|
|
509
|
-
}
|
|
474
|
+
if (!changelog) return [` ${theme.fg("dim", t("welcome.readyPrompt"))}`];
|
|
510
475
|
|
|
511
476
|
const version = this.#latestChangelogVersion(changelog);
|
|
512
|
-
|
|
513
|
-
return [
|
|
514
|
-
` ${theme.fg("muted", `Updated to v${version}`)}`,
|
|
515
|
-
` ${theme.fg("dim", `Use ${theme.bold("/changelog")} for details`)}`,
|
|
516
|
-
].slice(0, rowLimit);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
const items = this.#changelogItems(changelog);
|
|
477
|
+
const items = this.options.collapseChangelog ? [] : this.#changelogItems(changelog);
|
|
520
478
|
if (items.length === 0) {
|
|
521
479
|
return [
|
|
522
|
-
`
|
|
523
|
-
`
|
|
480
|
+
` ${theme.fg("muted", `Updated to v${version}`)}`,
|
|
481
|
+
` ${theme.fg("dim", `Use ${theme.bold("/changelog")} for details`)}`,
|
|
524
482
|
].slice(0, rowLimit);
|
|
525
483
|
}
|
|
526
484
|
|
|
527
|
-
const
|
|
528
|
-
const textWidth = Math.max(1, width - visibleWidth(
|
|
529
|
-
const
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
visibleItems.push(` ${theme.fg("dim", `… ${theme.bold("/changelog")} for full notes`)}`);
|
|
485
|
+
const bullet = ` ${theme.md.bullet} `;
|
|
486
|
+
const textWidth = Math.max(1, width - visibleWidth(bullet));
|
|
487
|
+
const visibleCount = items.length > rowLimit ? Math.max(1, rowLimit - 1) : rowLimit;
|
|
488
|
+
const lines = items
|
|
489
|
+
.slice(0, visibleCount)
|
|
490
|
+
.map(item => `${theme.fg("accent", bullet)}${theme.fg("muted", this.#truncate(item, textWidth))}`);
|
|
491
|
+
if (items.length > lines.length && lines.length < rowLimit) {
|
|
492
|
+
lines.push(` ${theme.fg("dim", `… ${theme.bold("/changelog")} for full notes`)}`);
|
|
536
493
|
}
|
|
537
|
-
return
|
|
494
|
+
return lines;
|
|
538
495
|
}
|
|
539
496
|
|
|
540
497
|
#latestChangelogVersion(markdown: string): string {
|
|
@@ -551,9 +508,7 @@ export class WelcomeComponent implements Component {
|
|
|
551
508
|
inFence = !inFence;
|
|
552
509
|
continue;
|
|
553
510
|
}
|
|
554
|
-
if (inFence || !line || /^#{1,6}\s+/.test(line) || /^-{3,}$/.test(line))
|
|
555
|
-
continue;
|
|
556
|
-
}
|
|
511
|
+
if (inFence || !line || /^#{1,6}\s+/.test(line) || /^-{3,}$/.test(line)) continue;
|
|
557
512
|
const withoutBullet = line
|
|
558
513
|
.replace(/^[-*]\s+/, "")
|
|
559
514
|
.replace(/^\d+\.\s+/, "")
|
|
@@ -575,134 +530,86 @@ export class WelcomeComponent implements Component {
|
|
|
575
530
|
.trim();
|
|
576
531
|
}
|
|
577
532
|
|
|
578
|
-
|
|
579
|
-
return `${theme.fg("borderMuted", "[")} ${theme.fg(color, icon)} ${theme.fg("muted", text)} ${theme.fg(
|
|
580
|
-
"borderMuted",
|
|
581
|
-
"]",
|
|
582
|
-
)}`;
|
|
583
|
-
}
|
|
533
|
+
// ── Launch reveal ───────────────────────────────────────────────────────
|
|
584
534
|
|
|
585
|
-
/**
|
|
586
|
-
|
|
587
|
-
|
|
535
|
+
/** How many sections are visible, and which one is still settling, at this frame. */
|
|
536
|
+
/** Number of sections that have taken their colors; the rest are still drawn, but in dim ink. */
|
|
537
|
+
#revealState(sectionCount: number): { colored: number } {
|
|
538
|
+
if (this.#animStart == null) return { colored: sectionCount };
|
|
588
539
|
const elapsed = performance.now() - this.#animStart;
|
|
589
|
-
|
|
590
|
-
// Ease-out cubic so the spin decelerates into the resting state.
|
|
591
|
-
const progress = elapsed / INTRO_MS;
|
|
592
|
-
const eased = 1 - (1 - progress) ** 3;
|
|
593
|
-
// Sweep backward through INTRO_SWEEPS full rotations so the gradient
|
|
594
|
-
// visibly spins multiple times. `eased == 1` → phase = 0 = resting frame.
|
|
595
|
-
const phase = ((((1 - eased) * INTRO_SWEEPS) % 1) + 1) % 1;
|
|
596
|
-
// Shine traverses the diagonal at a steady pace, decoupled from the
|
|
597
|
-
// gradient phase so the two layers parallax. Strength fades out with
|
|
598
|
-
// the same ease-out curve so the highlight is gone by the resting frame.
|
|
599
|
-
const shinePos = (((progress * INTRO_SHINE_TRAVERSALS) % 1) + 1) % 1;
|
|
600
|
-
const shineStrength = (1 - eased) ** 1.5;
|
|
601
|
-
return gradientLogo(logoLines, phase, { strength: shineStrength, pos: shinePos });
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
#logoLines(): readonly string[] {
|
|
605
|
-
return BRAND_LOGO;
|
|
540
|
+
return { colored: Math.min(sectionCount, Math.floor((elapsed - SECTION_SETTLE_MS) / SECTION_STAGGER_MS) + 1) };
|
|
606
541
|
}
|
|
607
|
-
}
|
|
608
542
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
];
|
|
543
|
+
/**
|
|
544
|
+
* Every fact is on screen from the first frame; the intro only lets color
|
|
545
|
+
* spread top to bottom, like ink soaking in. Content never waits on the effect.
|
|
546
|
+
*/
|
|
547
|
+
#revealLines(sections: Section[], offset: number, reveal: { colored: number }): string[] {
|
|
548
|
+
const lines: string[] = [];
|
|
549
|
+
sections.forEach((section, index) => {
|
|
550
|
+
if (offset + index < reveal.colored) {
|
|
551
|
+
lines.push(...section.lines);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
for (const line of section.lines) lines.push(theme.fg("dim", Bun.stripANSI(line)));
|
|
555
|
+
});
|
|
556
|
+
return lines;
|
|
557
|
+
}
|
|
615
558
|
|
|
616
|
-
|
|
617
|
-
const GRADIENT_STOPS: ReadonlyArray<readonly [number, number, number]> = [
|
|
618
|
-
[6, 19, 33], // deep navy
|
|
619
|
-
[11, 79, 138], // ocean
|
|
620
|
-
[47, 155, 255], // octopus mantle blue
|
|
621
|
-
[94, 200, 255], // bright tentacle blue
|
|
622
|
-
[168, 224, 255], // sky highlight
|
|
623
|
-
];
|
|
559
|
+
// ── Layout helpers ──────────────────────────────────────────────────────
|
|
624
560
|
|
|
625
|
-
|
|
626
|
-
|
|
561
|
+
#clip(lines: string[], rows: number): string[] {
|
|
562
|
+
if (rows <= 0) return [];
|
|
563
|
+
if (lines.length <= rows) return lines;
|
|
564
|
+
if (rows === 1) return [theme.fg("dim", " …")];
|
|
565
|
+
return [...lines.slice(0, rows - 1), theme.fg("dim", " …")];
|
|
566
|
+
}
|
|
627
567
|
|
|
628
|
-
|
|
629
|
-
|
|
568
|
+
#truncate(text: string, width: number): string {
|
|
569
|
+
return visibleWidth(text) > width ? truncateToWidth(text, width) : text;
|
|
570
|
+
}
|
|
630
571
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
let bl = a[2] + (b[2] - a[2]) * f;
|
|
665
|
-
if (shineStrength > 0) {
|
|
666
|
-
const dist = Math.abs(t - shinePos);
|
|
667
|
-
const intensity = Math.max(0, 1 - dist / SHINE_HALF_WIDTH) * shineStrength;
|
|
668
|
-
if (intensity > 0) {
|
|
669
|
-
r += (255 - r) * intensity;
|
|
670
|
-
g += (255 - g) * intensity;
|
|
671
|
-
bl += (255 - bl) * intensity;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
return `\x1b[38;2;${Math.round(r)};${Math.round(g)};${Math.round(bl)}m`;
|
|
675
|
-
}
|
|
676
|
-
: (t: number): string => {
|
|
677
|
-
const ramp = GRADIENT_RAMP_256;
|
|
678
|
-
let idx = Math.min(ramp.length - 1, Math.max(0, Math.floor(t * (ramp.length - 1) + 0.5)));
|
|
679
|
-
if (shineStrength > 0) {
|
|
680
|
-
const dist = Math.abs(t - shinePos);
|
|
681
|
-
const intensity = Math.max(0, 1 - dist / SHINE_HALF_WIDTH) * shineStrength;
|
|
682
|
-
// Promote to the brightest ramp slot when the shine band peaks here.
|
|
683
|
-
if (intensity > 0.5) idx = ramp.length - 1;
|
|
684
|
-
}
|
|
685
|
-
return `\x1b[38;5;${ramp[idx]}m`;
|
|
686
|
-
};
|
|
687
|
-
return lines.map((line, y) => {
|
|
688
|
-
let result = "";
|
|
689
|
-
for (let x = 0; x < line.length; x++) {
|
|
690
|
-
const char = line[x];
|
|
691
|
-
if (char === " ") {
|
|
692
|
-
result += char;
|
|
693
|
-
continue;
|
|
694
|
-
}
|
|
695
|
-
// Diagonal: bottom-left (x=0, y=rows-1) → top-right (x=cols-1, y=0)
|
|
696
|
-
const base = (x + (rows - 1 - y)) / span;
|
|
697
|
-
const t = (((base + phase) % 1) + 1) % 1;
|
|
698
|
-
result += colorAt(t) + char + reset;
|
|
572
|
+
/** Keep the tail of a long path — the project directory is the part that identifies it. */
|
|
573
|
+
#shortenFromLeft(value: string, width: number): string {
|
|
574
|
+
if (visibleWidth(value) <= width) return value;
|
|
575
|
+
if (width <= 1) return "…";
|
|
576
|
+
let tail = value;
|
|
577
|
+
while (tail.length > 0 && visibleWidth(tail) > width - 1) tail = tail.slice(1);
|
|
578
|
+
return `…${tail}`;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** Fit string to exact width with native ANSI/wide-glyph truncation and padding. */
|
|
582
|
+
#fitToWidth(str: string, width: number): string {
|
|
583
|
+
const visLen = visibleWidth(str);
|
|
584
|
+
if (visLen > width) return truncateToWidth(str, width, null, true);
|
|
585
|
+
return str + padding(width - visLen);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
#rightGutterWidth(termWidth: number): number {
|
|
589
|
+
const configured = this.options.rightGutterWidth ?? 0;
|
|
590
|
+
if (!Number.isFinite(configured) || configured <= 0) return 0;
|
|
591
|
+
const gutterWidth = Math.floor(configured);
|
|
592
|
+
return Math.min(gutterWidth, Math.max(0, termWidth - 4));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
#withRightGutter(lines: string[], rightGutterWidth: number): string[] {
|
|
596
|
+
if (rightGutterWidth <= 0) return lines;
|
|
597
|
+
const gutter = padding(rightGutterWidth);
|
|
598
|
+
return lines.map(line => line + gutter);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
#targetRows(termWidth: number): number | undefined {
|
|
602
|
+
const viewportRows = this.options.getViewportRows?.();
|
|
603
|
+
if (typeof viewportRows !== "number" || !Number.isFinite(viewportRows) || viewportRows <= 0) {
|
|
604
|
+
return undefined;
|
|
699
605
|
}
|
|
700
|
-
|
|
701
|
-
|
|
606
|
+
const reservedRows = Math.max(0, Math.floor(this.options.getReservedBottomRows?.(termWidth) ?? 0));
|
|
607
|
+
return Math.max(0, Math.floor(viewportRows) - reservedRows);
|
|
608
|
+
}
|
|
702
609
|
}
|
|
703
610
|
|
|
704
|
-
/**
|
|
705
|
-
const INTRO_MS =
|
|
611
|
+
/** Upper bound on the reveal: enough for every section to land and settle. */
|
|
612
|
+
const INTRO_MS = 12 * SECTION_STAGGER_MS + SECTION_SETTLE_MS;
|
|
706
613
|
/** Resolve the intro cadence without making tests mutate global process state. */
|
|
707
614
|
export function resolveWelcomeIntroTickMs(
|
|
708
615
|
platform: NodeJS.Platform = process.platform,
|
|
@@ -713,14 +620,3 @@ export function resolveWelcomeIntroTickMs(
|
|
|
713
620
|
|
|
714
621
|
/** Render at 30fps directly, but cap native Windows multiplexers at 10fps to avoid ConPTY output backpressure. */
|
|
715
622
|
const INTRO_TICK_MS = resolveWelcomeIntroTickMs();
|
|
716
|
-
/** Number of full gradient rotations the sweep performs before settling. */
|
|
717
|
-
const INTRO_SWEEPS = 2.5;
|
|
718
|
-
/** Number of times the shine highlight crosses the diagonal across the intro. */
|
|
719
|
-
const INTRO_SHINE_TRAVERSALS = 3;
|
|
720
|
-
|
|
721
|
-
/** Resting gradient frames, cached for re-renders outside of the intro. */
|
|
722
|
-
const REST_FRAMES: Record<WelcomeLogoMode, readonly string[]> = {
|
|
723
|
-
unicode: gradientLogo(BRAND_LOGO, 0),
|
|
724
|
-
square: gradientLogo(BRAND_LOGO, 0),
|
|
725
|
-
ascii: gradientLogo(BRAND_LOGO, 0),
|
|
726
|
-
};
|