@xp266/dshtui 0.1.3 → 0.1.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/bin/dshtui.js +105 -20
- 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 +131 -123
- package/lib/{surface-DdxzIgIY.mjs → surface-B7w3R_kh.mjs} +2 -2
- package/package.json +1 -1
- package/src/chat/bridge.ts +1 -1
- package/src/chat/session-list.ts +10 -0
- 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
|
@@ -4,54 +4,139 @@
|
|
|
4
4
|
* carries this TUI, equivalent to `dsh --profile <profile> [args...]`.
|
|
5
5
|
*
|
|
6
6
|
* The dsh CLI's subcommands are hardcoded upstream, so the command lives in
|
|
7
|
-
* this package's bin
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* this package's bin. Profile resolution: DSH_TUI_PROFILE overrides; else the
|
|
8
|
+
* `dshtui` profile when it exists; else the one profile that already mounts
|
|
9
|
+
* this package (an existing profile carries the user's session history —
|
|
10
|
+
* bootstrapping a fresh one would hide it); else the `dshtui` profile is
|
|
11
|
+
* bootstrapped. Before an install or upgrade, the profile's
|
|
12
|
+
* minimumReleaseAgeExclude gains this package's name so pnpm's
|
|
13
|
+
* supply-chain age policy never rejects freshly published releases.
|
|
11
14
|
*
|
|
12
15
|
* This file must stay free of lib/ imports so it works from a global
|
|
13
16
|
* install, a profile copy, and a dev checkout alike.
|
|
14
17
|
*/
|
|
15
18
|
import { spawn, spawnSync } from 'node:child_process'
|
|
16
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
19
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
17
20
|
import { homedir } from 'node:os'
|
|
18
21
|
import { dirname, join } from 'node:path'
|
|
19
22
|
import { fileURLToPath } from 'node:url'
|
|
20
23
|
|
|
21
24
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
22
25
|
const manifest = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'))
|
|
23
|
-
const
|
|
26
|
+
const packageName = manifest.name
|
|
27
|
+
const windows = process.platform === 'win32'
|
|
24
28
|
const dshHome = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
25
|
-
const
|
|
29
|
+
const profilesDir = join(dshHome, 'profiles')
|
|
26
30
|
|
|
27
31
|
function fail(message) {
|
|
28
32
|
console.error(`dshtui: ${message}`)
|
|
29
33
|
process.exit(1)
|
|
30
34
|
}
|
|
31
35
|
|
|
36
|
+
const quote = arg => /^[A-Za-z0-9_@%+=:,./-]+$/.test(arg) ? arg : `"${arg.replaceAll('"', '\\"')}"`
|
|
32
37
|
// Windows resolves `dsh` through a .cmd shim, which only spawns with a
|
|
33
|
-
// shell; the command line is
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
// shell; the command line is one quoted string there because passing an
|
|
39
|
+
// args array alongside shell:true is deprecated (DEP0190).
|
|
40
|
+
function runDsh(args) {
|
|
41
|
+
return windows
|
|
42
|
+
? spawnSync(['dsh', ...args].map(quote).join(' '), { stdio: 'inherit', shell: true })
|
|
43
|
+
: spawnSync('dsh', args, { stdio: 'inherit' })
|
|
44
|
+
}
|
|
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
|
+
|
|
52
|
+
function readJson(path) {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(readFileSync(path, 'utf8'))
|
|
55
|
+
} catch {
|
|
56
|
+
return undefined
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function profileMountsPackage(profileDir) {
|
|
61
|
+
const profileManifest = readJson(join(profileDir, 'package.json'))
|
|
62
|
+
if (profileManifest === undefined) return false
|
|
63
|
+
const bundles = profileManifest?.dsh?.profile?.bundles ?? []
|
|
64
|
+
if (bundles.includes(packageName)) return true
|
|
65
|
+
return packageName in (profileManifest.dependencies ?? {})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function installedVersion(profileDir) {
|
|
69
|
+
return readJson(join(profileDir, 'node_modules', packageName, 'package.json'))?.version
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function versionLessThan(installed, wanted) {
|
|
73
|
+
const core = version => version.split('-')[0].split('.').map(Number)
|
|
74
|
+
const [a, b] = [core(installed), core(wanted)]
|
|
75
|
+
for (let index = 0; index < 3; index++) {
|
|
76
|
+
if ((a[index] ?? 0) !== (b[index] ?? 0)) return (a[index] ?? 0) < (b[index] ?? 0)
|
|
77
|
+
}
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function ensureReleaseAgeExclude(profileDir) {
|
|
82
|
+
// The exclude matches the bare package name, so every version of this
|
|
83
|
+
// package bypasses the age policy while the rest of the tree stays
|
|
84
|
+
// protected by it.
|
|
85
|
+
const workspaceFile = join(profileDir, 'pnpm-workspace.yaml')
|
|
86
|
+
let content = ''
|
|
87
|
+
try {
|
|
88
|
+
content = readFileSync(workspaceFile, 'utf8')
|
|
89
|
+
} catch {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
if (new RegExp(`- ['"]?${packageName.replace('/', '\\/')}['"]?\\s*$`, 'm').test(content)) return
|
|
93
|
+
const line = ` - '${packageName}'`
|
|
94
|
+
content = /^minimumReleaseAgeExclude:\s*$/m.test(content)
|
|
95
|
+
? content.replace(/^minimumReleaseAgeExclude:\s*$/m, `minimumReleaseAgeExclude:\n${line}`)
|
|
96
|
+
: `${content.trimEnd()}\nminimumReleaseAgeExclude:\n${line}\n`
|
|
97
|
+
writeFileSync(workspaceFile, content)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveProfile() {
|
|
101
|
+
const override = process.env.DSH_TUI_PROFILE
|
|
102
|
+
if (override !== undefined) return override
|
|
103
|
+
if (profileMountsPackage(join(profilesDir, 'dshtui'))) return 'dshtui'
|
|
104
|
+
const candidates = existsSync(profilesDir)
|
|
105
|
+
? readdirSync(profilesDir).filter(name => name !== 'node_modules' && profileMountsPackage(join(profilesDir, name)))
|
|
106
|
+
: []
|
|
107
|
+
if (candidates.length === 1) return candidates[0]
|
|
108
|
+
if (candidates.length > 1) {
|
|
109
|
+
fail(`multiple profiles mount ${packageName} (${candidates.join(', ')}); pick one with DSH_TUI_PROFILE=<name>`)
|
|
110
|
+
}
|
|
111
|
+
return 'dshtui'
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const profile = resolveProfile()
|
|
115
|
+
const profileDir = join(profilesDir, profile)
|
|
116
|
+
const dshProbe = probeDsh()
|
|
117
|
+
if (dshProbe.error !== undefined || dshProbe.status !== 0) {
|
|
38
118
|
fail('the dsh CLI was not found on PATH; install it with: npm install -g @deepseek-ai/dsh')
|
|
39
119
|
}
|
|
40
120
|
|
|
41
121
|
if (!existsSync(profileDir)) {
|
|
42
|
-
console.error(`dshtui: profile "${profile}" not found under ${
|
|
43
|
-
const bootstrap =
|
|
44
|
-
'dsh',
|
|
45
|
-
['plugin', '--profile', profile, 'add', `${manifest.name}@${manifest.version}`],
|
|
46
|
-
{ stdio: 'inherit', shell: process.platform === 'win32' },
|
|
47
|
-
)
|
|
122
|
+
console.error(`dshtui: profile "${profile}" not found under ${profilesDir}; installing ${packageName}@${manifest.version} into it`)
|
|
123
|
+
const bootstrap = runDsh(['plugin', '--profile', profile, 'add', `${packageName}@${manifest.version}`])
|
|
48
124
|
if (bootstrap.status !== 0) {
|
|
49
|
-
fail(`bootstrapping the profile failed;
|
|
125
|
+
fail(`bootstrapping the profile failed; run manually: dsh plugin --profile ${profile} add ${packageName}`)
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
const installed = installedVersion(profileDir)
|
|
129
|
+
if (installed !== undefined && versionLessThan(installed, manifest.version)) {
|
|
130
|
+
ensureReleaseAgeExclude(profileDir)
|
|
131
|
+
console.error(`dshtui: upgrading profile "${profile}": ${packageName} ${installed} -> ${manifest.version}`)
|
|
132
|
+
const upgrade = runDsh(['plugin', '--profile', profile, 'add', `${packageName}@${manifest.version}`])
|
|
133
|
+
if (upgrade.status !== 0) {
|
|
134
|
+
fail(`the upgrade failed; run manually: dsh plugin --profile ${profile} add ${packageName}@${manifest.version}`)
|
|
135
|
+
}
|
|
50
136
|
}
|
|
51
137
|
}
|
|
52
138
|
|
|
53
139
|
const argv = process.argv.slice(2)
|
|
54
|
-
const quote = arg => /^[A-Za-z0-9_@%+=:,./-]+$/.test(arg) ? arg : `"${arg.replaceAll('"', '\\"')}"`
|
|
55
140
|
const command = ['dsh', '--profile', profile, ...argv].map(quote).join(' ')
|
|
56
141
|
const child = spawn(windows ? command : 'dsh', windows ? [] : ['--profile', profile, ...argv], {
|
|
57
142
|
stdio: 'inherit',
|
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",
|
|
@@ -7193,6 +7202,14 @@ function sessionTime(session) {
|
|
|
7193
7202
|
* (one log read, then persisted), never a repeated full read.
|
|
7194
7203
|
*/
|
|
7195
7204
|
async function computeSessionList(ctx) {
|
|
7205
|
+
try {
|
|
7206
|
+
return await computeSessionListInner(ctx);
|
|
7207
|
+
} catch (error$2) {
|
|
7208
|
+
error("boot", `session list failed: ${error$2 instanceof Error ? error$2.message : String(error$2)}`);
|
|
7209
|
+
throw error$2;
|
|
7210
|
+
}
|
|
7211
|
+
}
|
|
7212
|
+
async function computeSessionListInner(ctx) {
|
|
7196
7213
|
const workspacePaths = collectWorkspacePaths(ctx);
|
|
7197
7214
|
const archived = collectArchivedIds(ctx);
|
|
7198
7215
|
const summaries = [];
|
|
@@ -9062,9 +9079,7 @@ function App({ bridge, screen, themeTick = 0 }) {
|
|
|
9062
9079
|
const permissionChrome = permissionModeInfo(permissionMode);
|
|
9063
9080
|
const inputHeight = panel === null ? layout.barHeight : panelHeight + 1;
|
|
9064
9081
|
const messageHeight = Math.max(1, rows - inputHeight - 1);
|
|
9065
|
-
const
|
|
9066
|
-
const displayMessages = messages.length === 0 ? [logoBubble] : messages;
|
|
9067
|
-
const total = rowIndexFor(displayMessages, columns).total;
|
|
9082
|
+
const total = rowIndexFor(messages, columns).total;
|
|
9068
9083
|
const { scrollTop, applyScroll, getScroll } = useScroll(total, messageHeight);
|
|
9069
9084
|
const startNewSession = () => {
|
|
9070
9085
|
if (!bridge) return;
|
|
@@ -9158,7 +9173,7 @@ function App({ bridge, screen, themeTick = 0 }) {
|
|
|
9158
9173
|
};
|
|
9159
9174
|
const selectionRef = useRef(null);
|
|
9160
9175
|
const { selection, messageAreaSelection, chromeSelection, setSelection, clearSelection } = useMouseSelection({
|
|
9161
|
-
messages
|
|
9176
|
+
messages,
|
|
9162
9177
|
columns,
|
|
9163
9178
|
rows,
|
|
9164
9179
|
scrollTop,
|
|
@@ -9203,7 +9218,7 @@ function App({ bridge, screen, themeTick = 0 }) {
|
|
|
9203
9218
|
if (key.ctrl && input === "c") {
|
|
9204
9219
|
if (selection) {
|
|
9205
9220
|
const text = copySelection(selection, {
|
|
9206
|
-
messageText: (sel) => selectionText(
|
|
9221
|
+
messageText: (sel) => selectionText(messages, columns, sel),
|
|
9207
9222
|
chromeText: (sel) => chromeSelectionText(sel)
|
|
9208
9223
|
});
|
|
9209
9224
|
if (text) writeClipboardText(text);
|
|
@@ -9228,7 +9243,7 @@ function App({ bridge, screen, themeTick = 0 }) {
|
|
|
9228
9243
|
children: [
|
|
9229
9244
|
/* @__PURE__ */ jsx(KeymapGate, {}),
|
|
9230
9245
|
/* @__PURE__ */ jsx(MessageList, {
|
|
9231
|
-
messages
|
|
9246
|
+
messages,
|
|
9232
9247
|
height: messageHeight,
|
|
9233
9248
|
width: columns,
|
|
9234
9249
|
scrollTop,
|
|
@@ -10965,13 +10980,6 @@ function warmRenderPipeline() {
|
|
|
10965
10980
|
renderMarkdown(MARKDOWN_FIXTURE, 96);
|
|
10966
10981
|
renderMarkdown(MARKDOWN_FIXTURE, 72);
|
|
10967
10982
|
const index = buildRowIndex([
|
|
10968
|
-
{
|
|
10969
|
-
kind: "home-logo",
|
|
10970
|
-
id: "warm-logo",
|
|
10971
|
-
lines: ["██ ████"],
|
|
10972
|
-
colors: ["#7aa2f7"],
|
|
10973
|
-
info: ["dsh 0.0.0", "dshtui 0.0.0"]
|
|
10974
|
-
},
|
|
10975
10983
|
{
|
|
10976
10984
|
kind: "bubble",
|
|
10977
10985
|
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/bridge.ts
CHANGED
|
@@ -46,7 +46,7 @@ import { applyTheme } from '../apply-theme.ts'
|
|
|
46
46
|
import { THEME_SETTINGS_NAMESPACE } from '../theme-settings.ts'
|
|
47
47
|
import { themeMode } from '../theme.ts'
|
|
48
48
|
import type { ThemeMode } from '../theme.ts'
|
|
49
|
-
import { warn } from '../log.ts'
|
|
49
|
+
import { error as logError, warn } from '../log.ts'
|
|
50
50
|
|
|
51
51
|
interface AttachmentsServiceLike {
|
|
52
52
|
saveImage(input: { data: Uint8Array; mediaType: string; name?: string }): Promise<ImageAttachmentRef>
|
package/src/chat/session-list.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { stat } from 'node:fs/promises'
|
|
|
2
2
|
import type { Context } from '@deepseek-ai/cordis'
|
|
3
3
|
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
4
4
|
import { textFromBlocks } from './blocks.ts'
|
|
5
|
+
import { error as logError } from '../log.ts'
|
|
5
6
|
import { isBlankSession } from './presets.ts'
|
|
6
7
|
|
|
7
8
|
export interface SessionSummary {
|
|
@@ -27,6 +28,15 @@ export function sessionTime(session: SessionSummary): number {
|
|
|
27
28
|
* (one log read, then persisted), never a repeated full read.
|
|
28
29
|
*/
|
|
29
30
|
export async function computeSessionList(ctx: Context): Promise<SessionSummary[]> {
|
|
31
|
+
try {
|
|
32
|
+
return await computeSessionListInner(ctx)
|
|
33
|
+
} catch (error) {
|
|
34
|
+
logError('boot', `session list failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
35
|
+
throw error
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function computeSessionListInner(ctx: Context): Promise<SessionSummary[]> {
|
|
30
40
|
const workspacePaths = collectWorkspacePaths(ctx)
|
|
31
41
|
const archived = collectArchivedIds(ctx)
|
|
32
42
|
const summaries: SessionSummary[] = []
|
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 },
|