@xp266/dshtui 0.1.4 → 0.1.6
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/bin/dshtui.js +7 -1
- package/lib/contract/index.d.mts +1 -1
- package/lib/dialog.d.mts +1 -1
- package/lib/dialog.mjs +1 -1
- package/lib/{index-DqnaSXqP.d.mts → index-D4_nxuuR.d.mts} +3 -13
- package/lib/index.mjs +143 -228
- package/lib/{surface-DdxzIgIY.mjs → surface-B7w3R_kh.mjs} +2 -2
- package/package.json +1 -1
- package/src/chat/session-list.ts +29 -139
- package/src/contract/index.ts +1 -1
- package/src/model/message.ts +2 -13
- package/src/theme.ts +2 -2
- package/src/ui/app.tsx +4 -10
- package/src/ui/home-logo.ts +34 -27
- package/src/ui/message/layout.ts +0 -28
- package/src/ui/message/message-list.tsx +41 -1
- package/src/ui/message/warmup.ts +0 -1
package/bin/dshtui.js
CHANGED
|
@@ -43,6 +43,12 @@ function runDsh(args) {
|
|
|
43
43
|
: spawnSync('dsh', args, { stdio: 'inherit' })
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function probeDsh() {
|
|
47
|
+
return windows
|
|
48
|
+
? spawnSync(['dsh', '--version'].map(quote).join(' '), { stdio: 'ignore', shell: true })
|
|
49
|
+
: spawnSync('dsh', ['--version'], { stdio: 'ignore' })
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
function readJson(path) {
|
|
47
53
|
try {
|
|
48
54
|
return JSON.parse(readFileSync(path, 'utf8'))
|
|
@@ -107,7 +113,7 @@ function resolveProfile() {
|
|
|
107
113
|
|
|
108
114
|
const profile = resolveProfile()
|
|
109
115
|
const profileDir = join(profilesDir, profile)
|
|
110
|
-
const dshProbe =
|
|
116
|
+
const dshProbe = probeDsh()
|
|
111
117
|
if (dshProbe.error !== undefined || dshProbe.status !== 0) {
|
|
112
118
|
fail('the dsh CLI was not found on PATH; install it with: npm install -g @deepseek-ai/dsh')
|
|
113
119
|
}
|
package/lib/contract/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { $ as ToolCallPresentation, A as MessageViewContribution, At as TuiToolsFace, B as PointerSession, Bt as MouseEventData, C as MarkdownBlockContext, Ct as TuiPaletteFace, D as MessageRendererContribution, Dt as TuiSessionSummary, E as MarkdownInlineContribution, Et as TuiServicesFace, F as PasteContext, Ft as WindowCommandSpec, G as SelectionExtractContext, H as PrismGrammarContribution, Ht as MessageKind, I as PasteHandlerContribution, It as WindowContribution, J as SpecialFieldCreateInput, K as SelectionTransformerContribution, L as PasteResult, Lt as WindowHandle, M as OverlayContribution, Mt as TuiWindowsFace, N as POINTER_PLUGIN_ORDER, Nt as WidgetDef, O as MessageRendererResult, Ot as TuiStartupFace, P as PaintArgs, Pt as WidgetKeyApi, Q as ThemePaletteContribution, R as PointerEventFrame, Rt as WindowProps, S as KeyBindingContribution, St as TuiMarkdownFace, T as MarkdownInlineContext, Tt as TuiSelectionFace, U as SelectionClipboardContribution, Ut as LineSelection, V as PointerUiContext, Vt as MouseEventType, W as SelectionDomainContribution, Wt as PendingImage, X as SpecialFieldStyle, Y as SpecialFieldFactory, Z as StatusLineContribution, _ as InputStatusContext, _t as TuiHomeLogoFace, a as CommandDef, at as ToolViewContribution, b as InteractionPanelComponentProps, bt as TuiKey, c as CustomMessageSeed, ct as TuiChatFace, d as DiffLine, dt as TuiCommandsFace, et as ToolReadLine, f as DiffLineKind, ft as TuiComposerFace, g as HomeLogoContribution, gt as TuiHintFace, h as HintMatcherContribution, ht as TuiFieldsFace, i as ClipboardBackendContribution, it as ToolViewCallContext, j as MessageViewRender, jt as TuiWidgetsFace, k as MessageViewContext, kt as TuiStartupSink, l as DialogItemKinds, lt as TuiChromeFace, m as HintEntry, mt as TuiExtensionPoint, n as ClickActions, nt as ToolResultPresentation, o as ComposerInsertSpec, ot as ToolViewDiff, p as HintArgsContribution, pt as TuiContentFace, q as SpecialFieldContribution, r as ClickHit, rt as ToolResultView, s as CustomMessage, st as ToolViewResultContext, t as ChatNodeDefinition, tt as ToolReadView, u as DiffHunk, ut as TuiClipboardFace, v as InputStatusContribution, vt as TuiInteractionPanelsFace, w as MarkdownBlockContribution, wt as TuiPointerFace, x as InteractionPanelContribution, xt as TuiKeymapFace, y as InputStatusPart, yt as TuiInteractionsFace, z as PointerHandlerContribution, zt as ScrollSnapshot } from "../index-
|
|
1
|
+
import { $ as ToolCallPresentation, A as MessageViewContribution, At as TuiToolsFace, B as PointerSession, Bt as MouseEventData, C as MarkdownBlockContext, Ct as TuiPaletteFace, D as MessageRendererContribution, Dt as TuiSessionSummary, E as MarkdownInlineContribution, Et as TuiServicesFace, F as PasteContext, Ft as WindowCommandSpec, G as SelectionExtractContext, H as PrismGrammarContribution, Ht as MessageKind, I as PasteHandlerContribution, It as WindowContribution, J as SpecialFieldCreateInput, K as SelectionTransformerContribution, L as PasteResult, Lt as WindowHandle, M as OverlayContribution, Mt as TuiWindowsFace, N as POINTER_PLUGIN_ORDER, Nt as WidgetDef, O as MessageRendererResult, Ot as TuiStartupFace, P as PaintArgs, Pt as WidgetKeyApi, Q as ThemePaletteContribution, R as PointerEventFrame, Rt as WindowProps, S as KeyBindingContribution, St as TuiMarkdownFace, T as MarkdownInlineContext, Tt as TuiSelectionFace, U as SelectionClipboardContribution, Ut as LineSelection, V as PointerUiContext, Vt as MouseEventType, W as SelectionDomainContribution, Wt as PendingImage, X as SpecialFieldStyle, Y as SpecialFieldFactory, Z as StatusLineContribution, _ as InputStatusContext, _t as TuiHomeLogoFace, a as CommandDef, at as ToolViewContribution, b as InteractionPanelComponentProps, bt as TuiKey, c as CustomMessageSeed, ct as TuiChatFace, d as DiffLine, dt as TuiCommandsFace, et as ToolReadLine, f as DiffLineKind, ft as TuiComposerFace, g as HomeLogoContribution, gt as TuiHintFace, h as HintMatcherContribution, ht as TuiFieldsFace, i as ClipboardBackendContribution, it as ToolViewCallContext, j as MessageViewRender, jt as TuiWidgetsFace, k as MessageViewContext, kt as TuiStartupSink, l as DialogItemKinds, lt as TuiChromeFace, m as HintEntry, mt as TuiExtensionPoint, n as ClickActions, nt as ToolResultPresentation, o as ComposerInsertSpec, ot as ToolViewDiff, p as HintArgsContribution, pt as TuiContentFace, q as SpecialFieldContribution, r as ClickHit, rt as ToolResultView, s as CustomMessage, st as ToolViewResultContext, t as ChatNodeDefinition, tt as ToolReadView, u as DiffHunk, ut as TuiClipboardFace, v as InputStatusContribution, vt as TuiInteractionPanelsFace, w as MarkdownBlockContribution, wt as TuiPointerFace, x as InteractionPanelContribution, xt as TuiKeymapFace, y as InputStatusPart, yt as TuiInteractionsFace, z as PointerHandlerContribution, zt as ScrollSnapshot } from "../index-D4_nxuuR.mjs";
|
|
2
2
|
export { ChatNodeDefinition, ClickActions, ClickHit, ClipboardBackendContribution, CommandDef, ComposerInsertSpec, CustomMessage, CustomMessageSeed, DialogItemKinds, DiffHunk, DiffLine, DiffLineKind, HintArgsContribution, HintEntry, HintMatcherContribution, HomeLogoContribution, InputStatusContext, InputStatusContribution, InputStatusPart, InteractionPanelComponentProps, InteractionPanelContribution, KeyBindingContribution, type LineSelection, MarkdownBlockContext, MarkdownBlockContribution, MarkdownInlineContext, MarkdownInlineContribution, type MessageKind, MessageRendererContribution, MessageRendererResult, MessageViewContext, MessageViewContribution, MessageViewRender, type MouseEventData, type MouseEventType, OverlayContribution, POINTER_PLUGIN_ORDER, PaintArgs, PasteContext, PasteHandlerContribution, PasteResult, type PendingImage, PointerEventFrame, PointerHandlerContribution, PointerSession, PointerUiContext, PrismGrammarContribution, type ScrollSnapshot, SelectionClipboardContribution, SelectionDomainContribution, SelectionExtractContext, SelectionTransformerContribution, SpecialFieldContribution, SpecialFieldCreateInput, SpecialFieldFactory, SpecialFieldStyle, StatusLineContribution, ThemePaletteContribution, ToolCallPresentation, ToolReadLine, ToolReadView, ToolResultPresentation, ToolResultView, ToolViewCallContext, ToolViewContribution, ToolViewDiff, ToolViewResultContext, TuiChatFace, TuiChromeFace, TuiClipboardFace, TuiCommandsFace, TuiComposerFace, TuiContentFace, TuiExtensionPoint, TuiFieldsFace, TuiHintFace, TuiHomeLogoFace, TuiInteractionPanelsFace, TuiInteractionsFace, TuiKey, TuiKeymapFace, TuiMarkdownFace, TuiPaletteFace, TuiPointerFace, TuiSelectionFace, TuiServicesFace, TuiSessionSummary, TuiStartupFace, TuiStartupSink, TuiToolsFace, TuiWidgetsFace, TuiWindowsFace, WidgetDef, WidgetKeyApi, WindowCommandSpec, WindowContribution, WindowHandle, WindowProps };
|
package/lib/dialog.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Gt as Segment, Lt as WindowHandle, Nt as WidgetDef, l as DialogItemKinds } from "./index-
|
|
1
|
+
import { Gt as Segment, Lt as WindowHandle, Nt as WidgetDef, l as DialogItemKinds } from "./index-D4_nxuuR.mjs";
|
|
2
2
|
import { ReactNode, Ref } from "react";
|
|
3
3
|
//#region src/ui/dialog/items.d.ts
|
|
4
4
|
interface DialogRow {
|
package/lib/dialog.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import "./prod-react-Dzof4qJx.mjs";
|
|
2
|
-
import { I as panelAnchorRow, c as asTextItem, i as Dialog, l as registerWidget, n as panelLegend, o as rowHeight, r as CloseGuardContext, s as wrapStatusLines, t as PanelSurface } from "./surface-
|
|
2
|
+
import { I as panelAnchorRow, c as asTextItem, i as Dialog, l as registerWidget, n as panelLegend, o as rowHeight, r as CloseGuardContext, s as wrapStatusLines, t as PanelSurface } from "./surface-B7w3R_kh.mjs";
|
|
3
3
|
export { CloseGuardContext, Dialog, PanelSurface, asTextItem, panelAnchorRow, panelLegend, registerWidget, rowHeight, wrapStatusLines };
|
|
@@ -124,18 +124,8 @@ interface CompactionMessage {
|
|
|
124
124
|
error?: string;
|
|
125
125
|
streaming?: boolean;
|
|
126
126
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
id: string;
|
|
130
|
-
/** Artwork lines after culling; empty when the terminal cannot fit the logo. */
|
|
131
|
-
lines: readonly string[];
|
|
132
|
-
/** One gradient color per artwork line, aligned with `lines`. */
|
|
133
|
-
colors: readonly string[];
|
|
134
|
-
/** Version banner lines rendered under the artwork inside the same bubble. */
|
|
135
|
-
info: readonly string[];
|
|
136
|
-
}
|
|
137
|
-
type MessageKind = 'bubble' | 'collapsible' | 'tool-card' | 'compaction' | 'custom' | 'home-logo';
|
|
138
|
-
type Message = BubbleMessage | CollapsibleMessage | ToolCardMessage | CompactionMessage | CustomMessage | HomeLogoMessage;
|
|
127
|
+
type MessageKind = 'bubble' | 'collapsible' | 'tool-card' | 'compaction' | 'custom';
|
|
128
|
+
type Message = BubbleMessage | CollapsibleMessage | ToolCardMessage | CompactionMessage | CustomMessage;
|
|
139
129
|
//#endregion
|
|
140
130
|
//#region src/terminal/mouse.d.ts
|
|
141
131
|
type MouseEventType = 'down' | 'up' | 'drag' | 'move' | 'scroll';
|
|
@@ -439,7 +429,7 @@ interface InputStatusContribution {
|
|
|
439
429
|
interface HomeLogoContribution {
|
|
440
430
|
id: string;
|
|
441
431
|
order?: number;
|
|
442
|
-
/** Logo artwork
|
|
432
|
+
/** Logo artwork centered on the empty home page, one string per terminal row. */
|
|
443
433
|
lines: readonly string[];
|
|
444
434
|
}
|
|
445
435
|
interface TuiHomeLogoFace {
|
package/lib/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "./prod-react-Dzof4qJx.mjs";
|
|
2
|
-
import { $ as errorText, A as SelectableText, At as setColorLevel, B as inputFrameTop, C as caretNonceText, Ct as registerPalette, D as markKeymapConsumed, Dt as themeMode, E as isKeyConsumed, Et as subscribePalettes, F as messageBackgroundWidth, Ft as warn, G as wrapSegments, H as glyphs, I as panelAnchorRow, J as clampFocusRow, K as chromeSelectionText, L as panelContains, M as Region, Mt as configureLogs, N as hintSpan, Nt as error, O as createMouseController, Ot as keyedRegistry, P as inputContentContains, Pt as installCrashHandlers, Q as colToCharIndex, R as scrollbarColumn, S as caretNonceColor, St as permissionModeInfo, T as beginKeyEvent, Tt as setThemeMode, U as mergeRuns, V as inputStatusRow, W as segmentsKey, X as toScreenSelection, Y as selectedRange, Z as charWidth, _ as editDeleteWordRight, _t as releaseFields, a as DIALOG_MAX_HEIGHT, at as wrapLines, b as writeCursorShape, bt as COLORS, ct as fieldExpandOf, d as editCursorLeft, dt as hasFieldChar, et as lineBreaks, f as editCursorRight, ft as hasFieldSlots, g as editDeleteWordLeft, gt as registerFieldKind, h as editDelete, ht as linesChipLabel, i as Dialog, it as truncate, j as SelectionContext, jt as env, k as isMouseResidue, kt as colorLevel, l as registerWidget, lt as fieldSlotOf, m as editCursorWordRight, mt as isFieldChar, n as panelLegend, nt as segmentGraphemes, ot as allocateField, p as editCursorWordLeft, pt as imageChipLabel, q as sliceByColumns, r as CloseGuardContext, rt as textWidth, st as charactersChipLabel, t as PanelSurface, tt as locToPoint, u as editBackspace, ut as fieldStyleOf, v as editInsert, vt as releaseUnreferenced, w as useCaret, wt as replacePalettes, x as caretNonceBold, xt as paletteColor, y as CapText, yt as specialFieldFactory, z as hintBlockTop } from "./surface-
|
|
2
|
+
import { $ as errorText, A as SelectableText, At as setColorLevel, B as inputFrameTop, C as caretNonceText, Ct as registerPalette, D as markKeymapConsumed, Dt as themeMode, E as isKeyConsumed, Et as subscribePalettes, F as messageBackgroundWidth, Ft as warn, G as wrapSegments, H as glyphs, I as panelAnchorRow, J as clampFocusRow, K as chromeSelectionText, L as panelContains, M as Region, Mt as configureLogs, N as hintSpan, Nt as error, O as createMouseController, Ot as keyedRegistry, P as inputContentContains, Pt as installCrashHandlers, Q as colToCharIndex, R as scrollbarColumn, S as caretNonceColor, St as permissionModeInfo, T as beginKeyEvent, Tt as setThemeMode, U as mergeRuns, V as inputStatusRow, W as segmentsKey, X as toScreenSelection, Y as selectedRange, Z as charWidth, _ as editDeleteWordRight, _t as releaseFields, a as DIALOG_MAX_HEIGHT, at as wrapLines, b as writeCursorShape, bt as COLORS, ct as fieldExpandOf, d as editCursorLeft, dt as hasFieldChar, et as lineBreaks, f as editCursorRight, ft as hasFieldSlots, g as editDeleteWordLeft, gt as registerFieldKind, h as editDelete, ht as linesChipLabel, i as Dialog, it as truncate, j as SelectionContext, jt as env, k as isMouseResidue, kt as colorLevel, l as registerWidget, lt as fieldSlotOf, m as editCursorWordRight, mt as isFieldChar, n as panelLegend, nt as segmentGraphemes, ot as allocateField, p as editCursorWordLeft, pt as imageChipLabel, q as sliceByColumns, r as CloseGuardContext, rt as textWidth, st as charactersChipLabel, t as PanelSurface, tt as locToPoint, u as editBackspace, ut as fieldStyleOf, v as editInsert, vt as releaseUnreferenced, w as useCaret, wt as replacePalettes, x as caretNonceBold, xt as paletteColor, y as CapText, yt as specialFieldFactory, z as hintBlockTop } from "./surface-B7w3R_kh.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { readFileSync, statSync } from "node:fs";
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
@@ -2499,23 +2499,6 @@ function renderBody(message, width) {
|
|
|
2499
2499
|
bgs: null
|
|
2500
2500
|
};
|
|
2501
2501
|
}
|
|
2502
|
-
case "home-logo": {
|
|
2503
|
-
const lines = [];
|
|
2504
|
-
const rows = [];
|
|
2505
|
-
message.lines.forEach((line, i) => {
|
|
2506
|
-
lines.push(line);
|
|
2507
|
-
rows.push([{
|
|
2508
|
-
text: line,
|
|
2509
|
-
style: { color: message.colors[i] }
|
|
2510
|
-
}]);
|
|
2511
|
-
});
|
|
2512
|
-
for (const line of message.info) lines.push(truncate(line, Math.max(8, inner)));
|
|
2513
|
-
return {
|
|
2514
|
-
lines,
|
|
2515
|
-
rows: rows.length === 0 ? null : rows,
|
|
2516
|
-
bgs: null
|
|
2517
|
-
};
|
|
2518
|
-
}
|
|
2519
2502
|
case "custom": {
|
|
2520
2503
|
const view = messageViewOf(message.view);
|
|
2521
2504
|
if (view === void 0) return {
|
|
@@ -2659,13 +2642,6 @@ function planFor(message, body) {
|
|
|
2659
2642
|
blankRow()
|
|
2660
2643
|
];
|
|
2661
2644
|
}
|
|
2662
|
-
case "home-logo": return [...bodyRows(body.lines.length, {
|
|
2663
|
-
colStart: 4,
|
|
2664
|
-
bg: false,
|
|
2665
|
-
muted: false,
|
|
2666
|
-
selectable: true,
|
|
2667
|
-
role: "assistant"
|
|
2668
|
-
}), blankRow()];
|
|
2669
2645
|
case "custom": return [
|
|
2670
2646
|
{
|
|
2671
2647
|
type: "header",
|
|
@@ -2734,12 +2710,6 @@ function fingerprintOf(message) {
|
|
|
2734
2710
|
running: message.running,
|
|
2735
2711
|
streaming: message.streaming
|
|
2736
2712
|
};
|
|
2737
|
-
case "home-logo": return {
|
|
2738
|
-
kind: "home-logo",
|
|
2739
|
-
lines: message.lines,
|
|
2740
|
-
colors: message.colors,
|
|
2741
|
-
info: message.info
|
|
2742
|
-
};
|
|
2743
2713
|
}
|
|
2744
2714
|
}
|
|
2745
2715
|
function fingerprintMatches(entry, message) {
|
|
@@ -2749,7 +2719,6 @@ function fingerprintMatches(entry, message) {
|
|
|
2749
2719
|
case "tool-card": return message.kind === "tool-card" && entry.tool === message.tool && entry.label === message.label && entry.argsBody === message.argsBody && entry.resultBody === message.resultBody && entry.diff === message.diff && entry.read === message.read && entry.nested === message.nested && entry.error === message.error && entry.failed === message.failed && entry.exitCode === message.exitCode && entry.signal === message.signal && entry.bodyCol === message.bodyCol && entry.running === message.running && entry.streaming === message.streaming;
|
|
2750
2720
|
case "compaction": return message.kind === "compaction" && entry.summary === message.summary && entry.running === message.running && entry.error === message.error;
|
|
2751
2721
|
case "custom": return message.kind === "custom" && entry.view === message.view && entry.data === message.data && entry.running === message.running && entry.streaming === message.streaming;
|
|
2752
|
-
case "home-logo": return message.kind === "home-logo" && entry.lines === message.lines && entry.colors === message.colors && entry.info === message.info;
|
|
2753
2722
|
}
|
|
2754
2723
|
}
|
|
2755
2724
|
function renderFor(message, width) {
|
|
@@ -6146,59 +6115,6 @@ function backgroundFor(role) {
|
|
|
6146
6115
|
return role === "user" ? COLORS.userBubbleBackground : COLORS.aiBubbleBackground;
|
|
6147
6116
|
}
|
|
6148
6117
|
//#endregion
|
|
6149
|
-
//#region src/ui/message/message-list.tsx
|
|
6150
|
-
function MessageList({ messages, height, width, scrollTop, onScroll, interactive = true, themeTick = 0 }) {
|
|
6151
|
-
const index = rowIndexFor(messages, width);
|
|
6152
|
-
const total = index.total;
|
|
6153
|
-
const halfPage = Math.max(1, Math.ceil(height / 2));
|
|
6154
|
-
useInput((input, key) => {
|
|
6155
|
-
if (!interactive || isKeyConsumed()) return;
|
|
6156
|
-
const maxScroll = Math.max(0, total - height);
|
|
6157
|
-
const scroll = (delta) => {
|
|
6158
|
-
onScroll(Math.max(0, Math.min(maxScroll, scrollTop + delta)));
|
|
6159
|
-
};
|
|
6160
|
-
if (key.pageUp) scroll(-(height - 2));
|
|
6161
|
-
if (key.pageDown) scroll(height - 2);
|
|
6162
|
-
if (key.ctrl && input === "u") scroll(-halfPage);
|
|
6163
|
-
if (key.ctrl && input === "d") scroll(halfPage);
|
|
6164
|
-
});
|
|
6165
|
-
const rows = [];
|
|
6166
|
-
const endRow = Math.min(scrollTop + height, total);
|
|
6167
|
-
for (let row = scrollTop; row < endRow; row++) {
|
|
6168
|
-
const info = index.rowAt(row);
|
|
6169
|
-
if (info) rows.push(/* @__PURE__ */ jsx(MessageRow, {
|
|
6170
|
-
info,
|
|
6171
|
-
row,
|
|
6172
|
-
themeTick
|
|
6173
|
-
}, `${info.messageId}:${info.lineNo}`));
|
|
6174
|
-
}
|
|
6175
|
-
const scrollbar = scrollbarGeometry(total, height, scrollTop);
|
|
6176
|
-
return /* @__PURE__ */ jsxs(Box, {
|
|
6177
|
-
width,
|
|
6178
|
-
height,
|
|
6179
|
-
flexDirection: "column",
|
|
6180
|
-
overflow: "hidden",
|
|
6181
|
-
children: [/* @__PURE__ */ jsx(Region, {
|
|
6182
|
-
y: -scrollTop,
|
|
6183
|
-
children: rows
|
|
6184
|
-
}), scrollbar !== null && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Box, {
|
|
6185
|
-
position: "absolute",
|
|
6186
|
-
top: 0,
|
|
6187
|
-
left: scrollbarColumn(width),
|
|
6188
|
-
width: 1,
|
|
6189
|
-
height,
|
|
6190
|
-
backgroundColor: COLORS.scrollTrackBackground
|
|
6191
|
-
}), /* @__PURE__ */ jsx(Box, {
|
|
6192
|
-
position: "absolute",
|
|
6193
|
-
top: scrollbar.top,
|
|
6194
|
-
left: scrollbarColumn(width),
|
|
6195
|
-
width: 1,
|
|
6196
|
-
height: scrollbar.height,
|
|
6197
|
-
backgroundColor: COLORS.scrollThumbBackground
|
|
6198
|
-
})] })]
|
|
6199
|
-
});
|
|
6200
|
-
}
|
|
6201
|
-
//#endregion
|
|
6202
6118
|
//#region src/contract/upstream.ts
|
|
6203
6119
|
const UPSTREAM_FLOOR_VERSION = "0.1.2-rc.1";
|
|
6204
6120
|
const UPSTREAM_CEILING_VERSION = "0.2.0";
|
|
@@ -6405,34 +6321,41 @@ function homeLogoLineColors(lines, top, bottom) {
|
|
|
6405
6321
|
const step = 1 / (count - 1);
|
|
6406
6322
|
return lines.map((_, i) => mixHex(top, bottom, i * step));
|
|
6407
6323
|
}
|
|
6408
|
-
const
|
|
6324
|
+
const BLOCK_GLYPHS = /* @__PURE__ */ new Set([
|
|
6325
|
+
"█",
|
|
6326
|
+
"▀",
|
|
6327
|
+
"▄",
|
|
6328
|
+
" "
|
|
6329
|
+
]);
|
|
6409
6330
|
/**
|
|
6410
|
-
*
|
|
6411
|
-
*
|
|
6412
|
-
*
|
|
6413
|
-
*
|
|
6414
|
-
*
|
|
6331
|
+
* Displace pure block-glyph artwork down by half a cell: every cell's
|
|
6332
|
+
* top/bottom half ink moves one pixel-row down and is re-encoded as
|
|
6333
|
+
* ▀/▄/█. The shape is pixel-identical, but the artwork's bottom edge lands
|
|
6334
|
+
* on a cell boundary, so vertically centered text beside it reads as
|
|
6335
|
+
* aligned with the bottom line. Artwork containing any non-block glyph is
|
|
6336
|
+
* returned unchanged — text glyphs cannot be split across cells.
|
|
6415
6337
|
*/
|
|
6416
|
-
function
|
|
6417
|
-
const
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
]
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6338
|
+
function shiftLogoDownHalfRow(lines) {
|
|
6339
|
+
const width = lines.reduce((max, line) => Math.max(max, line.length), 0);
|
|
6340
|
+
if (width === 0) return [...lines];
|
|
6341
|
+
for (const line of lines) for (const ch of line) if (!BLOCK_GLYPHS.has(ch)) return [...lines];
|
|
6342
|
+
const top = lines.map((line) => Array.from({ length: width }, (_, c) => line[c] === "█" || line[c] === "▀"));
|
|
6343
|
+
const bottom = lines.map((line) => Array.from({ length: width }, (_, c) => line[c] === "█" || line[c] === "▄"));
|
|
6344
|
+
const rows = [];
|
|
6345
|
+
for (let r = 0; r <= lines.length; r++) {
|
|
6346
|
+
let row = "";
|
|
6347
|
+
let ink = false;
|
|
6348
|
+
for (let c = 0; c < width; c++) {
|
|
6349
|
+
const t = r > 0 && bottom[r - 1][c];
|
|
6350
|
+
const b = r < lines.length && top[r][c];
|
|
6351
|
+
row += t && b ? "█" : t ? "▀" : b ? "▄" : " ";
|
|
6352
|
+
ink = ink || t || b;
|
|
6353
|
+
}
|
|
6354
|
+
if (ink) rows.push(row.trimEnd());
|
|
6355
|
+
}
|
|
6356
|
+
return rows;
|
|
6435
6357
|
}
|
|
6358
|
+
/** The running harness version, `dsh <version>`. */
|
|
6436
6359
|
function dshVersionLine() {
|
|
6437
6360
|
return `dsh ${installedUpstreamLines()[0] ?? "unknown"}`;
|
|
6438
6361
|
}
|
|
@@ -6448,6 +6371,92 @@ function tuiVersion() {
|
|
|
6448
6371
|
return cachedTuiVersion;
|
|
6449
6372
|
}
|
|
6450
6373
|
//#endregion
|
|
6374
|
+
//#region src/ui/message/message-list.tsx
|
|
6375
|
+
/** Gap between the artwork's right edge and an appended version label. */
|
|
6376
|
+
const VERSION_GAP = 2;
|
|
6377
|
+
function MessageList({ messages, height, width, scrollTop, onScroll, interactive = true, themeTick = 0 }) {
|
|
6378
|
+
const index = rowIndexFor(messages, width);
|
|
6379
|
+
const total = index.total;
|
|
6380
|
+
const halfPage = Math.max(1, Math.ceil(height / 2));
|
|
6381
|
+
useInput((input, key) => {
|
|
6382
|
+
if (!interactive || isKeyConsumed()) return;
|
|
6383
|
+
const maxScroll = Math.max(0, total - height);
|
|
6384
|
+
const scroll = (delta) => {
|
|
6385
|
+
onScroll(Math.max(0, Math.min(maxScroll, scrollTop + delta)));
|
|
6386
|
+
};
|
|
6387
|
+
if (key.pageUp) scroll(-(height - 2));
|
|
6388
|
+
if (key.pageDown) scroll(height - 2);
|
|
6389
|
+
if (key.ctrl && input === "u") scroll(-halfPage);
|
|
6390
|
+
if (key.ctrl && input === "d") scroll(halfPage);
|
|
6391
|
+
});
|
|
6392
|
+
const logo = useHomeLogo();
|
|
6393
|
+
if (messages.length === 0) {
|
|
6394
|
+
const clean = shiftLogoDownHalfRow(logo?.lines.map((line) => line.trimEnd()) ?? []);
|
|
6395
|
+
const metrics = homeLogoMetrics(clean);
|
|
6396
|
+
const dshtuiVersionLine = `dshtui ${tuiVersion()}`;
|
|
6397
|
+
const versionWidth = Math.max(dshVersionLine().length, dshtuiVersionLine.length);
|
|
6398
|
+
const anchorWidth = clean.length >= 2 ? Math.max(textWidth(clean[clean.length - 2].trimEnd()), textWidth(clean[clean.length - 1].trimEnd())) : metrics.columns;
|
|
6399
|
+
const fits = logo !== void 0 && width >= 2 * (anchorWidth + VERSION_GAP + versionWidth) - metrics.columns && height >= metrics.rows;
|
|
6400
|
+
const colors = homeLogoLineColors(clean, COLORS.homeLogoTop, COLORS.homeLogoBottom);
|
|
6401
|
+
const left = Math.floor((width - metrics.columns) / 2);
|
|
6402
|
+
const rowText = (i) => {
|
|
6403
|
+
const artwork = clean[i].padEnd(i >= clean.length - 2 ? anchorWidth : metrics.columns);
|
|
6404
|
+
if (i === clean.length - 1) return `${artwork}${" ".repeat(VERSION_GAP)}${dshtuiVersionLine}`;
|
|
6405
|
+
if (i === clean.length - 2) return `${artwork}${" ".repeat(VERSION_GAP)}${dshVersionLine()}`;
|
|
6406
|
+
return artwork;
|
|
6407
|
+
};
|
|
6408
|
+
return /* @__PURE__ */ jsx(Box, {
|
|
6409
|
+
width,
|
|
6410
|
+
height,
|
|
6411
|
+
flexDirection: "column",
|
|
6412
|
+
justifyContent: "center",
|
|
6413
|
+
overflow: "hidden",
|
|
6414
|
+
children: fits && colors.map((color, i) => /* @__PURE__ */ jsx(Box, {
|
|
6415
|
+
marginLeft: left,
|
|
6416
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
6417
|
+
color,
|
|
6418
|
+
children: rowText(i)
|
|
6419
|
+
})
|
|
6420
|
+
}, i))
|
|
6421
|
+
});
|
|
6422
|
+
}
|
|
6423
|
+
const rows = [];
|
|
6424
|
+
const endRow = Math.min(scrollTop + height, total);
|
|
6425
|
+
for (let row = scrollTop; row < endRow; row++) {
|
|
6426
|
+
const info = index.rowAt(row);
|
|
6427
|
+
if (info) rows.push(/* @__PURE__ */ jsx(MessageRow, {
|
|
6428
|
+
info,
|
|
6429
|
+
row,
|
|
6430
|
+
themeTick
|
|
6431
|
+
}, `${info.messageId}:${info.lineNo}`));
|
|
6432
|
+
}
|
|
6433
|
+
const scrollbar = scrollbarGeometry(total, height, scrollTop);
|
|
6434
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
6435
|
+
width,
|
|
6436
|
+
height,
|
|
6437
|
+
flexDirection: "column",
|
|
6438
|
+
overflow: "hidden",
|
|
6439
|
+
children: [/* @__PURE__ */ jsx(Region, {
|
|
6440
|
+
y: -scrollTop,
|
|
6441
|
+
children: rows
|
|
6442
|
+
}), scrollbar !== null && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Box, {
|
|
6443
|
+
position: "absolute",
|
|
6444
|
+
top: 0,
|
|
6445
|
+
left: scrollbarColumn(width),
|
|
6446
|
+
width: 1,
|
|
6447
|
+
height,
|
|
6448
|
+
backgroundColor: COLORS.scrollTrackBackground
|
|
6449
|
+
}), /* @__PURE__ */ jsx(Box, {
|
|
6450
|
+
position: "absolute",
|
|
6451
|
+
top: scrollbar.top,
|
|
6452
|
+
left: scrollbarColumn(width),
|
|
6453
|
+
width: 1,
|
|
6454
|
+
height: scrollbar.height,
|
|
6455
|
+
backgroundColor: COLORS.scrollThumbBackground
|
|
6456
|
+
})] })]
|
|
6457
|
+
});
|
|
6458
|
+
}
|
|
6459
|
+
//#endregion
|
|
6451
6460
|
//#region src/chat/models.ts
|
|
6452
6461
|
const API_PROTOCOLS = [
|
|
6453
6462
|
"openai-completions",
|
|
@@ -7188,9 +7197,9 @@ function sessionTime(session) {
|
|
|
7188
7197
|
* with the client-side visibility rules: live sessions plus persisted cold
|
|
7189
7198
|
* sessions with a cwd, blank and subagent-origin rows hidden, newest-first,
|
|
7190
7199
|
* without any workspace filtering (the picker labels ungrouped sessions).
|
|
7191
|
-
* Cold rows
|
|
7192
|
-
*
|
|
7193
|
-
*
|
|
7200
|
+
* Cold rows take their titles from the host's batched session-title fold
|
|
7201
|
+
* (`sessionQuery.readTitleSnapshots`), which reads each log once per
|
|
7202
|
+
* release of the query corpus and never loads full sessions into memory.
|
|
7194
7203
|
*/
|
|
7195
7204
|
async function computeSessionList(ctx) {
|
|
7196
7205
|
try {
|
|
@@ -7215,8 +7224,7 @@ async function computeSessionListInner(ctx) {
|
|
|
7215
7224
|
const title = titleService?.get?.(session)?.title ?? firstUserText(session.snapshotEvents());
|
|
7216
7225
|
summaries.push(toSummary(id, title, session.header.cwd, session.header.createdAt, lastPromptAt(session.snapshotEvents()), workspacePaths));
|
|
7217
7226
|
}
|
|
7218
|
-
|
|
7219
|
-
await mergeColdSummaries(ctx, persistence, await listColdHeaders(ctx, persistence, attached), archived, workspacePaths, summaries);
|
|
7227
|
+
await mergeColdSummaries(ctx, await listColdHeaders(ctx, ctx.get("sessionPersistence"), attached), archived, workspacePaths, summaries);
|
|
7220
7228
|
await attachModifiedTimes(ctx, summaries);
|
|
7221
7229
|
return summaries.sort((a, b) => sessionTime(b) - sessionTime(a));
|
|
7222
7230
|
}
|
|
@@ -7277,102 +7285,26 @@ function lastPromptAt(events) {
|
|
|
7277
7285
|
for (const event of events) if (event.type === "user/message" && event.data.source.kind === "user") last = event.time;
|
|
7278
7286
|
return last;
|
|
7279
7287
|
}
|
|
7280
|
-
|
|
7281
|
-
const
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
function listMetadataHint(values) {
|
|
7294
|
-
const hint = values.sessionListMetadata;
|
|
7295
|
-
if (typeof hint?.blank !== "boolean") return void 0;
|
|
7296
|
-
return {
|
|
7297
|
-
blank: hint.blank,
|
|
7298
|
-
lastPromptAt: typeof hint.lastPromptAt === "number" ? hint.lastPromptAt : null
|
|
7299
|
-
};
|
|
7300
|
-
}
|
|
7301
|
-
/**
|
|
7302
|
-
* Cold-session metadata without unconditional full-log reads: projection
|
|
7303
|
-
* hints first (zero I/O), then a direct read only when the artifact is small
|
|
7304
|
-
* enough to be a blank-session log. A hint-less oversized artifact is
|
|
7305
|
-
* conservatively visible; its title comes from the projection cache's cold
|
|
7306
|
-
* ladder, which reads the log once and writes the row back durably.
|
|
7307
|
-
*/
|
|
7308
|
-
async function resolveColdMeta(cache, persistence, header) {
|
|
7309
|
-
let titleHint;
|
|
7310
|
-
let metadata;
|
|
7311
|
-
try {
|
|
7312
|
-
const values = cache?.cachedSnapshot?.(header)?.values ?? {};
|
|
7313
|
-
if (typeof values.title === "string" && values.title !== "") titleHint = values.title;
|
|
7314
|
-
metadata = listMetadataHint(values);
|
|
7315
|
-
} catch {}
|
|
7316
|
-
if (metadata?.blank === false) return {
|
|
7317
|
-
title: await coldTitle(cache, header, titleHint),
|
|
7318
|
-
blank: false,
|
|
7319
|
-
promptAt: metadata.lastPromptAt ?? 0
|
|
7320
|
-
};
|
|
7321
|
-
const probed = await probeSmallArtifact(persistence, header);
|
|
7322
|
-
if (probed !== void 0) return {
|
|
7323
|
-
title: probed.title ?? titleHint,
|
|
7324
|
-
blank: probed.blank,
|
|
7325
|
-
promptAt: probed.promptAt
|
|
7326
|
-
};
|
|
7327
|
-
return {
|
|
7328
|
-
title: await coldTitle(cache, header, titleHint),
|
|
7329
|
-
blank: false,
|
|
7330
|
-
promptAt: metadata?.lastPromptAt ?? 0
|
|
7331
|
-
};
|
|
7332
|
-
}
|
|
7333
|
-
async function coldTitle(cache, header, titleHint) {
|
|
7334
|
-
if (titleHint !== void 0 || cache?.coldSnapshot === void 0) return titleHint;
|
|
7335
|
-
try {
|
|
7336
|
-
const title = (await cache.coldSnapshot(String(header.id)))?.values?.title;
|
|
7337
|
-
return typeof title === "string" && title !== "" ? title : void 0;
|
|
7338
|
-
} catch {
|
|
7339
|
-
return;
|
|
7340
|
-
}
|
|
7341
|
-
}
|
|
7342
|
-
async function probeSmallArtifact(persistence, header) {
|
|
7343
|
-
if (persistence.locate === void 0 || persistence.readFrom === void 0) return void 0;
|
|
7344
|
-
const location = persistence.locate(header);
|
|
7345
|
-
if (location === void 0) return void 0;
|
|
7346
|
-
try {
|
|
7347
|
-
if ((await stat(location.path)).size > COLD_PROBE_MAX_BYTES) return void 0;
|
|
7348
|
-
const { events } = await persistence.readFrom(String(header.id), 0);
|
|
7349
|
-
return {
|
|
7350
|
-
blank: isBlankSession(events),
|
|
7351
|
-
title: titleFromEvents(events) ?? firstUserText(events),
|
|
7352
|
-
promptAt: lastPromptAt(events)
|
|
7353
|
-
};
|
|
7354
|
-
} catch {
|
|
7355
|
-
return;
|
|
7288
|
+
async function mergeColdSummaries(ctx, cold, archived, workspacePaths, summaries) {
|
|
7289
|
+
const query = ctx.get("sessionQuery");
|
|
7290
|
+
if (query?.readTitleSnapshots === void 0) return;
|
|
7291
|
+
const observed = await query.readTitleSnapshots(cold.map((header) => String(header.id))).catch(() => []);
|
|
7292
|
+
const titleById = /* @__PURE__ */ new Map();
|
|
7293
|
+
for (const entry of observed) {
|
|
7294
|
+
if (entry.status !== "fulfilled") continue;
|
|
7295
|
+
const snapshot = entry.value?.title;
|
|
7296
|
+
if (snapshot?.title === void 0 || snapshot.title === "") continue;
|
|
7297
|
+
titleById.set(String(entry.sessionId), {
|
|
7298
|
+
title: snapshot.title,
|
|
7299
|
+
promptAt: snapshot.updatedAt ?? 0
|
|
7300
|
+
});
|
|
7356
7301
|
}
|
|
7357
|
-
}
|
|
7358
|
-
async function mergeColdSummaries(ctx, persistence, cold, archived, workspacePaths, summaries) {
|
|
7359
|
-
if (persistence === void 0) return;
|
|
7360
|
-
const cache = ctx.get("sessionProjectionCache");
|
|
7361
|
-
const pending = [];
|
|
7362
7302
|
for (const header of cold) {
|
|
7363
7303
|
if (archived.has(String(header.id))) continue;
|
|
7364
|
-
const
|
|
7365
|
-
if (
|
|
7366
|
-
|
|
7367
|
-
continue;
|
|
7368
|
-
}
|
|
7369
|
-
pending.push(header);
|
|
7304
|
+
const meta = titleById.get(String(header.id));
|
|
7305
|
+
if (meta === void 0) continue;
|
|
7306
|
+
summaries.push(toSummary(String(header.id), meta.title, header.cwd, header.createdAt, meta.promptAt, workspacePaths));
|
|
7370
7307
|
}
|
|
7371
|
-
for (let index = 0; index < pending.length; index += PROBE_BATCH_SIZE) await Promise.all(pending.slice(index, index + PROBE_BATCH_SIZE).map(async (header) => {
|
|
7372
|
-
const meta = await resolveColdMeta(cache, persistence, header);
|
|
7373
|
-
cacheSessionMeta(String(header.id), meta);
|
|
7374
|
-
if (!meta.blank) summaries.push(toSummary(String(header.id), meta.title, header.cwd, header.createdAt, meta.promptAt, workspacePaths));
|
|
7375
|
-
}));
|
|
7376
7308
|
}
|
|
7377
7309
|
function fallbackName(id, directory) {
|
|
7378
7310
|
if (directory !== void 0 && directory !== "") {
|
|
@@ -7381,14 +7313,6 @@ function fallbackName(id, directory) {
|
|
|
7381
7313
|
}
|
|
7382
7314
|
return id.match(/[^/]+$/)?.[0] ?? id;
|
|
7383
7315
|
}
|
|
7384
|
-
function titleFromEvents(events) {
|
|
7385
|
-
let title;
|
|
7386
|
-
for (const event of events) if (event.type === "session/title") {
|
|
7387
|
-
const data = event.data;
|
|
7388
|
-
if (data?.title !== void 0 && data.title !== "") title = data.title;
|
|
7389
|
-
}
|
|
7390
|
-
return title;
|
|
7391
|
-
}
|
|
7392
7316
|
function firstUserText(events) {
|
|
7393
7317
|
for (const event of events) {
|
|
7394
7318
|
if (event.type !== "user/message") continue;
|
|
@@ -9070,9 +8994,7 @@ function App({ bridge, screen, themeTick = 0 }) {
|
|
|
9070
8994
|
const permissionChrome = permissionModeInfo(permissionMode);
|
|
9071
8995
|
const inputHeight = panel === null ? layout.barHeight : panelHeight + 1;
|
|
9072
8996
|
const messageHeight = Math.max(1, rows - inputHeight - 1);
|
|
9073
|
-
const
|
|
9074
|
-
const displayMessages = messages.length === 0 ? [logoBubble] : messages;
|
|
9075
|
-
const total = rowIndexFor(displayMessages, columns).total;
|
|
8997
|
+
const total = rowIndexFor(messages, columns).total;
|
|
9076
8998
|
const { scrollTop, applyScroll, getScroll } = useScroll(total, messageHeight);
|
|
9077
8999
|
const startNewSession = () => {
|
|
9078
9000
|
if (!bridge) return;
|
|
@@ -9166,7 +9088,7 @@ function App({ bridge, screen, themeTick = 0 }) {
|
|
|
9166
9088
|
};
|
|
9167
9089
|
const selectionRef = useRef(null);
|
|
9168
9090
|
const { selection, messageAreaSelection, chromeSelection, setSelection, clearSelection } = useMouseSelection({
|
|
9169
|
-
messages
|
|
9091
|
+
messages,
|
|
9170
9092
|
columns,
|
|
9171
9093
|
rows,
|
|
9172
9094
|
scrollTop,
|
|
@@ -9211,7 +9133,7 @@ function App({ bridge, screen, themeTick = 0 }) {
|
|
|
9211
9133
|
if (key.ctrl && input === "c") {
|
|
9212
9134
|
if (selection) {
|
|
9213
9135
|
const text = copySelection(selection, {
|
|
9214
|
-
messageText: (sel) => selectionText(
|
|
9136
|
+
messageText: (sel) => selectionText(messages, columns, sel),
|
|
9215
9137
|
chromeText: (sel) => chromeSelectionText(sel)
|
|
9216
9138
|
});
|
|
9217
9139
|
if (text) writeClipboardText(text);
|
|
@@ -9236,7 +9158,7 @@ function App({ bridge, screen, themeTick = 0 }) {
|
|
|
9236
9158
|
children: [
|
|
9237
9159
|
/* @__PURE__ */ jsx(KeymapGate, {}),
|
|
9238
9160
|
/* @__PURE__ */ jsx(MessageList, {
|
|
9239
|
-
messages
|
|
9161
|
+
messages,
|
|
9240
9162
|
height: messageHeight,
|
|
9241
9163
|
width: columns,
|
|
9242
9164
|
scrollTop,
|
|
@@ -10973,13 +10895,6 @@ function warmRenderPipeline() {
|
|
|
10973
10895
|
renderMarkdown(MARKDOWN_FIXTURE, 96);
|
|
10974
10896
|
renderMarkdown(MARKDOWN_FIXTURE, 72);
|
|
10975
10897
|
const index = buildRowIndex([
|
|
10976
|
-
{
|
|
10977
|
-
kind: "home-logo",
|
|
10978
|
-
id: "warm-logo",
|
|
10979
|
-
lines: ["██ ████"],
|
|
10980
|
-
colors: ["#7aa2f7"],
|
|
10981
|
-
info: ["dsh 0.0.0", "dshtui 0.0.0"]
|
|
10982
|
-
},
|
|
10983
10898
|
{
|
|
10984
10899
|
kind: "bubble",
|
|
10985
10900
|
id: "warm-user",
|
|
@@ -505,8 +505,8 @@ function buildPalette(ladder, hues, code, mode) {
|
|
|
505
505
|
thinkCodeType: thinkCode.type,
|
|
506
506
|
thinkCodeVariable: thinkCode.variable,
|
|
507
507
|
thinkCodeConstant: thinkCode.variable,
|
|
508
|
-
homeLogoTop: mode === "dark" ? "#
|
|
509
|
-
homeLogoBottom: mode === "dark" ? "#
|
|
508
|
+
homeLogoTop: mode === "dark" ? "#e1e1e1" : "#7f7f7f",
|
|
509
|
+
homeLogoBottom: mode === "dark" ? "#6c6c6c" : "#292929"
|
|
510
510
|
};
|
|
511
511
|
}
|
|
512
512
|
const darkPalette = buildPalette(DARK_LADDER, DARK_HUES, DARK_CODE, "dark");
|
package/package.json
CHANGED
package/src/chat/session-list.ts
CHANGED
|
@@ -23,9 +23,9 @@ export function sessionTime(session: SessionSummary): number {
|
|
|
23
23
|
* with the client-side visibility rules: live sessions plus persisted cold
|
|
24
24
|
* sessions with a cwd, blank and subagent-origin rows hidden, newest-first,
|
|
25
25
|
* without any workspace filtering (the picker labels ungrouped sessions).
|
|
26
|
-
* Cold rows
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* Cold rows take their titles from the host's batched session-title fold
|
|
27
|
+
* (`sessionQuery.readTitleSnapshots`), which reads each log once per
|
|
28
|
+
* release of the query corpus and never loads full sessions into memory.
|
|
29
29
|
*/
|
|
30
30
|
export async function computeSessionList(ctx: Context): Promise<SessionSummary[]> {
|
|
31
31
|
try {
|
|
@@ -55,7 +55,7 @@ async function computeSessionListInner(ctx: Context): Promise<SessionSummary[]>
|
|
|
55
55
|
}
|
|
56
56
|
const persistence = ctx.get('sessionPersistence') as PersistenceLike | undefined
|
|
57
57
|
const cold = await listColdHeaders(ctx, persistence, attached)
|
|
58
|
-
await mergeColdSummaries(ctx,
|
|
58
|
+
await mergeColdSummaries(ctx, cold, archived, workspacePaths, summaries)
|
|
59
59
|
await attachModifiedTimes(ctx, summaries)
|
|
60
60
|
return summaries.sort((a, b) => sessionTime(b) - sessionTime(a))
|
|
61
61
|
}
|
|
@@ -67,13 +67,18 @@ interface ColdHeader {
|
|
|
67
67
|
origin?: string
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
interface
|
|
71
|
-
|
|
70
|
+
interface PersistenceLike {
|
|
71
|
+
list(signal?: AbortSignal): Promise<ColdHeader[]>
|
|
72
|
+
locate?(header: { cwd?: string; id: string }): { path: string } | undefined
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
interface
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
interface SessionQueryLike {
|
|
76
|
+
listSessions?(signal?: AbortSignal): Promise<Array<{ header: ColdHeader; live: boolean; persisted: boolean }>>
|
|
77
|
+
readTitleSnapshots?(sessionIds: readonly string[]): Promise<Array<{
|
|
78
|
+
sessionId: string
|
|
79
|
+
status: 'fulfilled' | 'rejected'
|
|
80
|
+
value?: { title?: { title?: string; updatedAt?: number } }
|
|
81
|
+
}>>
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
/**
|
|
@@ -159,134 +164,30 @@ function lastPromptAt(events: readonly SessionEvent[]): number {
|
|
|
159
164
|
return last
|
|
160
165
|
}
|
|
161
166
|
|
|
162
|
-
interface PersistenceLike {
|
|
163
|
-
list(signal?: AbortSignal): Promise<ColdHeader[]>
|
|
164
|
-
locate?(header: { cwd?: string; id: string }): { path: string } | undefined
|
|
165
|
-
readFrom?(id: string, offset: number, signal?: AbortSignal): Promise<{ events: SessionEvent[] }>
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
interface SessionMetaCacheEntry {
|
|
169
|
-
title: string | undefined
|
|
170
|
-
blank: boolean | undefined
|
|
171
|
-
promptAt: number
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const SESSION_META_CACHE_MAX = 512
|
|
175
|
-
const sessionMetaCache = new Map<string, SessionMetaCacheEntry>()
|
|
176
|
-
|
|
177
|
-
function cacheSessionMeta(id: string, entry: SessionMetaCacheEntry): void {
|
|
178
|
-
while (sessionMetaCache.size >= SESSION_META_CACHE_MAX) {
|
|
179
|
-
const oldest = sessionMetaCache.keys().next()
|
|
180
|
-
if (oldest.done) break
|
|
181
|
-
sessionMetaCache.delete(oldest.value)
|
|
182
|
-
}
|
|
183
|
-
sessionMetaCache.set(id, entry)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const PROBE_BATCH_SIZE = 16
|
|
187
|
-
|
|
188
|
-
/** Mirrors the host's cold blank-probe gate: only small artifacts are read. */
|
|
189
|
-
const COLD_PROBE_MAX_BYTES = 1024
|
|
190
|
-
|
|
191
|
-
interface ColdMeta {
|
|
192
|
-
title: string | undefined
|
|
193
|
-
blank: boolean
|
|
194
|
-
promptAt: number
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function listMetadataHint(values: Record<string, unknown>): { blank: boolean; lastPromptAt: number | null } | undefined {
|
|
198
|
-
const hint = values.sessionListMetadata as { blank?: unknown; lastPromptAt?: unknown } | undefined
|
|
199
|
-
if (typeof hint?.blank !== 'boolean') return undefined
|
|
200
|
-
return {
|
|
201
|
-
blank: hint.blank,
|
|
202
|
-
lastPromptAt: typeof hint.lastPromptAt === 'number' ? hint.lastPromptAt : null,
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Cold-session metadata without unconditional full-log reads: projection
|
|
208
|
-
* hints first (zero I/O), then a direct read only when the artifact is small
|
|
209
|
-
* enough to be a blank-session log. A hint-less oversized artifact is
|
|
210
|
-
* conservatively visible; its title comes from the projection cache's cold
|
|
211
|
-
* ladder, which reads the log once and writes the row back durably.
|
|
212
|
-
*/
|
|
213
|
-
async function resolveColdMeta(cache: ProjectionCacheLike | undefined, persistence: PersistenceLike, header: ColdHeader): Promise<ColdMeta> {
|
|
214
|
-
let titleHint: string | undefined
|
|
215
|
-
let metadata: { blank: boolean; lastPromptAt: number | null } | undefined
|
|
216
|
-
try {
|
|
217
|
-
const values = cache?.cachedSnapshot?.(header)?.values ?? {}
|
|
218
|
-
if (typeof values.title === 'string' && values.title !== '') titleHint = values.title
|
|
219
|
-
metadata = listMetadataHint(values)
|
|
220
|
-
} catch {
|
|
221
|
-
// A broken projection cache must not block the session picker; fall back to probing.
|
|
222
|
-
}
|
|
223
|
-
if (metadata?.blank === false) {
|
|
224
|
-
return { title: await coldTitle(cache, header, titleHint), blank: false, promptAt: metadata.lastPromptAt ?? 0 }
|
|
225
|
-
}
|
|
226
|
-
const probed = await probeSmallArtifact(persistence, header)
|
|
227
|
-
if (probed !== undefined) {
|
|
228
|
-
return { title: probed.title ?? titleHint, blank: probed.blank, promptAt: probed.promptAt }
|
|
229
|
-
}
|
|
230
|
-
return { title: await coldTitle(cache, header, titleHint), blank: false, promptAt: metadata?.lastPromptAt ?? 0 }
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
async function coldTitle(cache: ProjectionCacheLike | undefined, header: ColdHeader, titleHint: string | undefined): Promise<string | undefined> {
|
|
234
|
-
if (titleHint !== undefined || cache?.coldSnapshot === undefined) return titleHint
|
|
235
|
-
try {
|
|
236
|
-
const snapshot = await cache.coldSnapshot(String(header.id))
|
|
237
|
-
const title = snapshot?.values?.title
|
|
238
|
-
return typeof title === 'string' && title !== '' ? title : undefined
|
|
239
|
-
} catch {
|
|
240
|
-
// A failed cold-snapshot read leaves the session without a title.
|
|
241
|
-
return undefined
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async function probeSmallArtifact(persistence: PersistenceLike, header: ColdHeader): Promise<ColdMeta | undefined> {
|
|
246
|
-
if (persistence.locate === undefined || persistence.readFrom === undefined) return undefined
|
|
247
|
-
const location = persistence.locate(header)
|
|
248
|
-
if (location === undefined) return undefined
|
|
249
|
-
try {
|
|
250
|
-
const info = await stat(location.path)
|
|
251
|
-
if (info.size > COLD_PROBE_MAX_BYTES) return undefined
|
|
252
|
-
const { events } = await persistence.readFrom(String(header.id), 0)
|
|
253
|
-
return { blank: isBlankSession(events), title: titleFromEvents(events) ?? firstUserText(events), promptAt: lastPromptAt(events) }
|
|
254
|
-
} catch {
|
|
255
|
-
// An unreadable artifact is treated as a blank session (skipped in the list).
|
|
256
|
-
return undefined
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
167
|
async function mergeColdSummaries(
|
|
261
168
|
ctx: Context,
|
|
262
|
-
persistence: PersistenceLike | undefined,
|
|
263
169
|
cold: ColdHeader[],
|
|
264
170
|
archived: ReadonlySet<string>,
|
|
265
171
|
workspacePaths: ReadonlySet<string>,
|
|
266
172
|
summaries: SessionSummary[],
|
|
267
173
|
): Promise<void> {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
174
|
+
const query = ctx.get('sessionQuery') as SessionQueryLike | undefined
|
|
175
|
+
if (query?.readTitleSnapshots === undefined) return
|
|
176
|
+
const observed = await query.readTitleSnapshots(cold.map(header => String(header.id))).catch(() => [])
|
|
177
|
+
const titleById = new Map<string, { title: string; promptAt: number }>()
|
|
178
|
+
for (const entry of observed) {
|
|
179
|
+
if (entry.status !== 'fulfilled') continue
|
|
180
|
+
const snapshot = entry.value?.title
|
|
181
|
+
if (snapshot?.title === undefined || snapshot.title === '') continue
|
|
182
|
+
titleById.set(String(entry.sessionId), { title: snapshot.title, promptAt: snapshot.updatedAt ?? 0 })
|
|
183
|
+
}
|
|
271
184
|
for (const header of cold) {
|
|
272
185
|
if (archived.has(String(header.id))) continue
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
continue
|
|
279
|
-
}
|
|
280
|
-
pending.push(header)
|
|
281
|
-
}
|
|
282
|
-
for (let index = 0; index < pending.length; index += PROBE_BATCH_SIZE) {
|
|
283
|
-
await Promise.all(pending.slice(index, index + PROBE_BATCH_SIZE).map(async header => {
|
|
284
|
-
const meta = await resolveColdMeta(cache, persistence, header)
|
|
285
|
-
cacheSessionMeta(String(header.id), meta)
|
|
286
|
-
if (!meta.blank) {
|
|
287
|
-
summaries.push(toSummary(String(header.id), meta.title, header.cwd, header.createdAt, meta.promptAt, workspacePaths))
|
|
288
|
-
}
|
|
289
|
-
}))
|
|
186
|
+
// No title snapshot means the session never ran a turn: the host's
|
|
187
|
+
// deterministic fallback titles every session with a user message.
|
|
188
|
+
const meta = titleById.get(String(header.id))
|
|
189
|
+
if (meta === undefined) continue
|
|
190
|
+
summaries.push(toSummary(String(header.id), meta.title, header.cwd, header.createdAt, meta.promptAt, workspacePaths))
|
|
290
191
|
}
|
|
291
192
|
}
|
|
292
193
|
|
|
@@ -299,17 +200,6 @@ function fallbackName(id: string, directory?: string): string {
|
|
|
299
200
|
return match?.[0] ?? id
|
|
300
201
|
}
|
|
301
202
|
|
|
302
|
-
function titleFromEvents(events: readonly SessionEvent[]): string | undefined {
|
|
303
|
-
let title: string | undefined
|
|
304
|
-
for (const event of events) {
|
|
305
|
-
if ((event as { type?: string }).type === 'session/title') {
|
|
306
|
-
const data = (event as { data?: { title?: string } }).data
|
|
307
|
-
if (data?.title !== undefined && data.title !== '') title = data.title
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
return title
|
|
311
|
-
}
|
|
312
|
-
|
|
313
203
|
function firstUserText(events: readonly SessionEvent[]): string | undefined {
|
|
314
204
|
for (const event of events) {
|
|
315
205
|
if (event.type !== 'user/message') continue
|
package/src/contract/index.ts
CHANGED
|
@@ -304,7 +304,7 @@ export interface InputStatusContribution {
|
|
|
304
304
|
export interface HomeLogoContribution {
|
|
305
305
|
id: string
|
|
306
306
|
order?: number
|
|
307
|
-
/** Logo artwork
|
|
307
|
+
/** Logo artwork centered on the empty home page, one string per terminal row. */
|
|
308
308
|
lines: readonly string[]
|
|
309
309
|
}
|
|
310
310
|
|
package/src/model/message.ts
CHANGED
|
@@ -73,17 +73,6 @@ export interface CompactionMessage {
|
|
|
73
73
|
streaming?: boolean
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
export
|
|
77
|
-
kind: 'home-logo'
|
|
78
|
-
id: string
|
|
79
|
-
/** Artwork lines after culling; empty when the terminal cannot fit the logo. */
|
|
80
|
-
lines: readonly string[]
|
|
81
|
-
/** One gradient color per artwork line, aligned with `lines`. */
|
|
82
|
-
colors: readonly string[]
|
|
83
|
-
/** Version banner lines rendered under the artwork inside the same bubble. */
|
|
84
|
-
info: readonly string[]
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export type MessageKind = 'bubble' | 'collapsible' | 'tool-card' | 'compaction' | 'custom' | 'home-logo'
|
|
76
|
+
export type MessageKind = 'bubble' | 'collapsible' | 'tool-card' | 'compaction' | 'custom'
|
|
88
77
|
|
|
89
|
-
export type Message = BubbleMessage | CollapsibleMessage | ToolCardMessage | CompactionMessage | CustomMessage
|
|
78
|
+
export type Message = BubbleMessage | CollapsibleMessage | ToolCardMessage | CompactionMessage | CustomMessage
|
package/src/theme.ts
CHANGED
|
@@ -246,8 +246,8 @@ function buildPalette(ladder: GrayLadder, hues: SemanticHues, code: CodeHues, mo
|
|
|
246
246
|
thinkCodeVariable: thinkCode.variable,
|
|
247
247
|
thinkCodeConstant: thinkCode.variable,
|
|
248
248
|
|
|
249
|
-
homeLogoTop: mode === 'dark' ? '#
|
|
250
|
-
homeLogoBottom: mode === 'dark' ? '#
|
|
249
|
+
homeLogoTop: mode === 'dark' ? '#e1e1e1' : '#7f7f7f',
|
|
250
|
+
homeLogoBottom: mode === 'dark' ? '#6c6c6c' : '#292929',
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
253
|
|
package/src/ui/app.tsx
CHANGED
|
@@ -30,7 +30,6 @@ import { useComposer } from './input/use-composer.ts'
|
|
|
30
30
|
import type { ComposerSubmission } from './input/composer-fields.ts'
|
|
31
31
|
import { Region } from './region.tsx'
|
|
32
32
|
import { MessageList } from './message/message-list.tsx'
|
|
33
|
-
import { useHomeLogoBubble } from './home-logo.ts'
|
|
34
33
|
import { SpinnerTickProvider } from './spinner-tick.tsx'
|
|
35
34
|
import { CloseGuardContext } from './dialog/dialog.tsx'
|
|
36
35
|
import type { DialogHandle } from './dialog/dialog.tsx'
|
|
@@ -185,12 +184,7 @@ export function App({ bridge, screen, themeTick = 0 }: AppProps) {
|
|
|
185
184
|
const bottomHeight = panel === null ? layout.barHeight : panelHeight + 1
|
|
186
185
|
const inputHeight = bottomHeight
|
|
187
186
|
const messageHeight = Math.max(1, rows - inputHeight - MESSAGE_INPUT_GAP_ROWS)
|
|
188
|
-
|
|
189
|
-
// bubble is its content. Any real message switches to the chat list and
|
|
190
|
-
// the logo disappears.
|
|
191
|
-
const logoBubble = useHomeLogoBubble(columns, messageHeight)
|
|
192
|
-
const displayMessages = messages.length === 0 ? [logoBubble] : messages
|
|
193
|
-
const total = rowIndexFor(displayMessages, columns).total
|
|
187
|
+
const total = rowIndexFor(messages, columns).total
|
|
194
188
|
const { scrollTop, applyScroll, getScroll } = useScroll(total, messageHeight)
|
|
195
189
|
const startNewSession = () => {
|
|
196
190
|
if (!bridge) return
|
|
@@ -284,7 +278,7 @@ export function App({ bridge, screen, themeTick = 0 }: AppProps) {
|
|
|
284
278
|
}
|
|
285
279
|
const selectionRef = useRef<LineSelection | null>(null)
|
|
286
280
|
const { selection, messageAreaSelection, chromeSelection, setSelection, clearSelection } = useMouseSelection({
|
|
287
|
-
messages
|
|
281
|
+
messages,
|
|
288
282
|
columns,
|
|
289
283
|
rows,
|
|
290
284
|
scrollTop,
|
|
@@ -329,7 +323,7 @@ export function App({ bridge, screen, themeTick = 0 }: AppProps) {
|
|
|
329
323
|
if (key.ctrl && input === 'c') {
|
|
330
324
|
if (selection) {
|
|
331
325
|
const text = copySelection(selection, {
|
|
332
|
-
messageText: sel => selectionText(
|
|
326
|
+
messageText: sel => selectionText(messages, columns, sel),
|
|
333
327
|
chromeText: sel => chromeSelectionText(sel),
|
|
334
328
|
})
|
|
335
329
|
if (text) writeClipboardText(text)
|
|
@@ -349,7 +343,7 @@ export function App({ bridge, screen, themeTick = 0 }: AppProps) {
|
|
|
349
343
|
<Box flexDirection="column" width={columns} height={rows}>
|
|
350
344
|
<KeymapGate />
|
|
351
345
|
<MessageList
|
|
352
|
-
messages={
|
|
346
|
+
messages={messages}
|
|
353
347
|
height={messageHeight}
|
|
354
348
|
width={columns}
|
|
355
349
|
scrollTop={scrollTop}
|
package/src/ui/home-logo.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs'
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import {
|
|
3
|
+
import { useSyncExternalStore } from 'react'
|
|
4
4
|
import { installedUpstreamLines } from '../contract/upstream.ts'
|
|
5
|
-
import { BUBBLE_WIDTH_OFFSET } from '../core/metrics.ts'
|
|
6
|
-
import type { HomeLogoMessage } from '../model/message.ts'
|
|
7
5
|
import { keyedRegistry } from '../kernel/registry.ts'
|
|
8
6
|
import { textWidth } from '../core/text.ts'
|
|
9
7
|
import { COLORS } from '../theme.ts'
|
|
@@ -83,34 +81,43 @@ export function homeLogoLineColors(lines: readonly string[], top: string, bottom
|
|
|
83
81
|
return lines.map((_, i) => mixHex(top, bottom, i * step))
|
|
84
82
|
}
|
|
85
83
|
|
|
86
|
-
const
|
|
84
|
+
const BLOCK_GLYPHS = new Set(['█', '▀', '▄', ' '])
|
|
87
85
|
|
|
88
86
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
87
|
+
* Displace pure block-glyph artwork down by half a cell: every cell's
|
|
88
|
+
* top/bottom half ink moves one pixel-row down and is re-encoded as
|
|
89
|
+
* ▀/▄/█. The shape is pixel-identical, but the artwork's bottom edge lands
|
|
90
|
+
* on a cell boundary, so vertically centered text beside it reads as
|
|
91
|
+
* aligned with the bottom line. Artwork containing any non-block glyph is
|
|
92
|
+
* returned unchanged — text glyphs cannot be split across cells.
|
|
94
93
|
*/
|
|
95
|
-
export function
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
94
|
+
export function shiftLogoDownHalfRow(lines: readonly string[]): string[] {
|
|
95
|
+
const width = lines.reduce((max, line) => Math.max(max, line.length), 0)
|
|
96
|
+
if (width === 0) return [...lines]
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
for (const ch of line) {
|
|
99
|
+
if (!BLOCK_GLYPHS.has(ch)) return [...lines]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const top: boolean[][] = lines.map(line => Array.from({ length: width }, (_, c) => line[c] === '█' || line[c] === '▀'))
|
|
103
|
+
const bottom: boolean[][] = lines.map(line => Array.from({ length: width }, (_, c) => line[c] === '█' || line[c] === '▄'))
|
|
104
|
+
const rows: string[] = []
|
|
105
|
+
for (let r = 0; r <= lines.length; r++) {
|
|
106
|
+
let row = ''
|
|
107
|
+
let ink = false
|
|
108
|
+
for (let c = 0; c < width; c++) {
|
|
109
|
+
const t = r > 0 && bottom[r - 1]![c]!
|
|
110
|
+
const b = r < lines.length && top[r]![c]!
|
|
111
|
+
row += t && b ? '█' : t ? '▀' : b ? '▄' : ' '
|
|
112
|
+
ink = ink || t || b
|
|
113
|
+
}
|
|
114
|
+
if (ink) rows.push(row.trimEnd())
|
|
110
115
|
}
|
|
116
|
+
return rows
|
|
111
117
|
}
|
|
112
118
|
|
|
113
|
-
|
|
119
|
+
/** The running harness version, `dsh <version>`. */
|
|
120
|
+
export function dshVersionLine(): string {
|
|
114
121
|
// dsh-agent itself does not export ./package.json, so the version comes
|
|
115
122
|
// from the lockstepped harness line any sibling package exposes.
|
|
116
123
|
return `dsh ${installedUpstreamLines()[0] ?? 'unknown'}`
|
|
@@ -118,14 +125,14 @@ function dshVersionLine(): string {
|
|
|
118
125
|
|
|
119
126
|
let cachedTuiVersion: string | undefined
|
|
120
127
|
|
|
121
|
-
function tuiVersion(): string {
|
|
128
|
+
export function tuiVersion(): string {
|
|
122
129
|
if (cachedTuiVersion !== undefined) return cachedTuiVersion
|
|
123
130
|
try {
|
|
124
131
|
const path = fileURLToPath(import.meta.resolve('@xp266/dshtui/package.json'))
|
|
125
132
|
const manifest = JSON.parse(readFileSync(path, 'utf8')) as { version?: string }
|
|
126
133
|
cachedTuiVersion = manifest.version ?? 'unknown'
|
|
127
134
|
} catch {
|
|
128
|
-
// The manifest may be unreadable in unusual install layouts; the
|
|
135
|
+
// The manifest may be unreadable in unusual install layouts; the label still renders.
|
|
129
136
|
cachedTuiVersion = 'unknown'
|
|
130
137
|
}
|
|
131
138
|
return cachedTuiVersion
|
package/src/ui/message/layout.ts
CHANGED
|
@@ -248,16 +248,6 @@ function renderBody(message: Message, width: number): BodyRendered {
|
|
|
248
248
|
}
|
|
249
249
|
return { lines, rows, bgs: null }
|
|
250
250
|
}
|
|
251
|
-
case 'home-logo': {
|
|
252
|
-
const lines: string[] = []
|
|
253
|
-
const rows: Segment[][] = []
|
|
254
|
-
message.lines.forEach((line, i) => {
|
|
255
|
-
lines.push(line)
|
|
256
|
-
rows.push([{ text: line, style: { color: message.colors[i] } }])
|
|
257
|
-
})
|
|
258
|
-
for (const line of message.info) lines.push(truncate(line, Math.max(8, inner)))
|
|
259
|
-
return { lines, rows: rows.length === 0 ? null : rows, bgs: null }
|
|
260
|
-
}
|
|
261
251
|
case 'custom': {
|
|
262
252
|
const view = messageViewOf(message.view)
|
|
263
253
|
if (view === undefined) {
|
|
@@ -369,11 +359,6 @@ function planFor(message: Message, body: BodyRendered): PlanRow[] {
|
|
|
369
359
|
blankRow(),
|
|
370
360
|
]
|
|
371
361
|
}
|
|
372
|
-
case 'home-logo':
|
|
373
|
-
return [
|
|
374
|
-
...bodyRows(body.lines.length, { colStart: 4, bg: false, muted: false, selectable: true, role: 'assistant' }),
|
|
375
|
-
blankRow(),
|
|
376
|
-
]
|
|
377
362
|
case 'custom': {
|
|
378
363
|
const header: PlanRow = {
|
|
379
364
|
type: 'header',
|
|
@@ -403,7 +388,6 @@ type RenderFingerprint =
|
|
|
403
388
|
| { kind: 'tool-card'; tool: string; label: string; argsBody: string; resultBody: string | undefined; diff: ToolCardMessage['diff']; read: ToolCardMessage['read']; nested: ToolCardMessage['nested']; error: string | undefined; failed: boolean | undefined; exitCode: number | undefined; signal: string | undefined; bodyCol: number | undefined; running: boolean; streaming: boolean | undefined }
|
|
404
389
|
| { kind: 'compaction'; summary: string; running: boolean; error: string | undefined }
|
|
405
390
|
| { kind: 'custom'; view: string; data: unknown; running: boolean | undefined; streaming: boolean | undefined }
|
|
406
|
-
| { kind: 'home-logo'; lines: readonly string[]; colors: readonly string[]; info: readonly string[] }
|
|
407
391
|
|
|
408
392
|
interface RenderMemoEntry {
|
|
409
393
|
epoch: number
|
|
@@ -467,13 +451,6 @@ function fingerprintOf(message: Message): RenderFingerprint {
|
|
|
467
451
|
running: message.running,
|
|
468
452
|
streaming: message.streaming,
|
|
469
453
|
}
|
|
470
|
-
case 'home-logo':
|
|
471
|
-
return {
|
|
472
|
-
kind: 'home-logo',
|
|
473
|
-
lines: message.lines,
|
|
474
|
-
colors: message.colors,
|
|
475
|
-
info: message.info,
|
|
476
|
-
}
|
|
477
454
|
}
|
|
478
455
|
}
|
|
479
456
|
|
|
@@ -520,11 +497,6 @@ function fingerprintMatches(entry: RenderFingerprint, message: Message): boolean
|
|
|
520
497
|
&& entry.data === message.data
|
|
521
498
|
&& entry.running === message.running
|
|
522
499
|
&& entry.streaming === message.streaming
|
|
523
|
-
case 'home-logo':
|
|
524
|
-
return message.kind === 'home-logo'
|
|
525
|
-
&& entry.lines === message.lines
|
|
526
|
-
&& entry.colors === message.colors
|
|
527
|
-
&& entry.info === message.info
|
|
528
500
|
}
|
|
529
501
|
}
|
|
530
502
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { Box, useInput } from 'ink'
|
|
1
|
+
import { Box, Text, useInput } from 'ink'
|
|
2
2
|
import { isKeyConsumed } from '../key-arbiter.ts'
|
|
3
3
|
import type { Message } from '../../model/message.ts'
|
|
4
4
|
import { rowIndexFor, scrollbarGeometry } from './layout.ts'
|
|
5
5
|
import { MessageRow } from './message-row.tsx'
|
|
6
|
+
import { textWidth } from '../../core/text.ts'
|
|
6
7
|
import { COLORS } from '../../theme.ts'
|
|
7
8
|
import { scrollbarColumn } from '../layout-service.ts'
|
|
8
9
|
import { Region } from '../region.tsx'
|
|
10
|
+
import { dshVersionLine, homeLogoLineColors, homeLogoMetrics, shiftLogoDownHalfRow, tuiVersion, useHomeLogo } from '../home-logo.ts'
|
|
9
11
|
|
|
10
12
|
interface MessageListProps {
|
|
11
13
|
messages: Message[]
|
|
@@ -17,6 +19,9 @@ interface MessageListProps {
|
|
|
17
19
|
themeTick?: number
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
/** Gap between the artwork's right edge and an appended version label. */
|
|
23
|
+
const VERSION_GAP = 2
|
|
24
|
+
|
|
20
25
|
export function MessageList({ messages, height, width, scrollTop, onScroll, interactive = true, themeTick = 0 }: MessageListProps) {
|
|
21
26
|
const index = rowIndexFor(messages, width)
|
|
22
27
|
const total = index.total
|
|
@@ -32,6 +37,41 @@ export function MessageList({ messages, height, width, scrollTop, onScroll, inte
|
|
|
32
37
|
if (key.ctrl && input === 'u') scroll(-halfPage)
|
|
33
38
|
if (key.ctrl && input === 'd') scroll(halfPage)
|
|
34
39
|
})
|
|
40
|
+
const logo = useHomeLogo()
|
|
41
|
+
if (messages.length === 0) {
|
|
42
|
+
// Shift the block artwork down half a cell so its bottom edge meets the
|
|
43
|
+
// vertically centered version text.
|
|
44
|
+
const clean = shiftLogoDownHalfRow(logo?.lines.map(line => line.trimEnd()) ?? [])
|
|
45
|
+
const metrics = homeLogoMetrics(clean)
|
|
46
|
+
const dshtuiVersionLine = `dshtui ${tuiVersion()}`
|
|
47
|
+
const versionWidth = Math.max(dshVersionLine().length, dshtuiVersionLine.length)
|
|
48
|
+
// The labels hug the bottom word's ink (narrower than the widest row);
|
|
49
|
+
// the anchor is the widest of the two label rows so the labels line up
|
|
50
|
+
// even when the shifted artwork leaves them different lengths.
|
|
51
|
+
const anchorWidth = clean.length >= 2
|
|
52
|
+
? Math.max(textWidth(clean[clean.length - 2]!.trimEnd()), textWidth(clean[clean.length - 1]!.trimEnd()))
|
|
53
|
+
: metrics.columns
|
|
54
|
+
// The version labels hang right of the centered artwork block, so the
|
|
55
|
+
// width must hold the label at its overhanging position.
|
|
56
|
+
const fits = logo !== undefined && width >= 2 * (anchorWidth + VERSION_GAP + versionWidth) - metrics.columns && height >= metrics.rows
|
|
57
|
+
const colors = homeLogoLineColors(clean, COLORS.homeLogoTop, COLORS.homeLogoBottom)
|
|
58
|
+
const left = Math.floor((width - metrics.columns) / 2)
|
|
59
|
+
const rowText = (i: number): string => {
|
|
60
|
+
const artwork = clean[i]!.padEnd(i >= clean.length - 2 ? anchorWidth : metrics.columns)
|
|
61
|
+
if (i === clean.length - 1) return `${artwork}${' '.repeat(VERSION_GAP)}${dshtuiVersionLine}`
|
|
62
|
+
if (i === clean.length - 2) return `${artwork}${' '.repeat(VERSION_GAP)}${dshVersionLine()}`
|
|
63
|
+
return artwork
|
|
64
|
+
}
|
|
65
|
+
return (
|
|
66
|
+
<Box width={width} height={height} flexDirection="column" justifyContent="center" overflow="hidden">
|
|
67
|
+
{fits && colors.map((color, i) => (
|
|
68
|
+
<Box key={i} marginLeft={left}>
|
|
69
|
+
<Text color={color}>{rowText(i)}</Text>
|
|
70
|
+
</Box>
|
|
71
|
+
))}
|
|
72
|
+
</Box>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
35
75
|
const rows = []
|
|
36
76
|
const endRow = Math.min(scrollTop + height, total)
|
|
37
77
|
for (let row = scrollTop; row < endRow; row++) {
|
package/src/ui/message/warmup.ts
CHANGED
|
@@ -28,7 +28,6 @@ export function warmRenderPipeline(): void {
|
|
|
28
28
|
renderMarkdown(MARKDOWN_FIXTURE, 96)
|
|
29
29
|
renderMarkdown(MARKDOWN_FIXTURE, 72)
|
|
30
30
|
const messages: Message[] = [
|
|
31
|
-
{ kind: 'home-logo', id: 'warm-logo', lines: ['██ ████'], colors: ['#7aa2f7'], info: ['dsh 0.0.0', 'dshtui 0.0.0'] },
|
|
32
31
|
{ kind: 'bubble', id: 'warm-user', role: 'user', content: 'warm up the render pipeline' },
|
|
33
32
|
{ kind: 'bubble', id: 'warm-ai', role: 'assistant', content: MARKDOWN_FIXTURE },
|
|
34
33
|
{ kind: 'collapsible', id: 'warm-think', label: 'Thinking', body: 'thinking fixture body', running: false, collapsed: false, thinking: true },
|