@itookit/dsht 0.6.3 → 0.6.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/README.i18n.yaml +2 -2
- package/README.md +10 -0
- package/README.zh.md +10 -0
- package/dist/ui/app.js +12 -4
- package/dist/ui/dialogs/index.d.ts +12 -0
- package/dist/ui/dialogs/index.js +12 -0
- package/dist/ui/offline.d.ts +38 -0
- package/dist/ui/offline.js +57 -0
- package/package.json +1 -1
package/README.i18n.yaml
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
# Git blob hashes of the reviewed bilingual pair.
|
|
2
|
-
README.md:
|
|
3
|
-
README.zh.md:
|
|
2
|
+
README.md: 1f4feedf3b3c23e6e415ad3d30873fbd4704b27f
|
|
3
|
+
README.zh.md: b0046be28c55bb63249c321ba6dff67eca091487
|
package/README.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# DeepSeek Harness Terminal
|
|
2
2
|
|
|
3
3
|
English | [中文](README.zh.md)
|
|
4
|
+
Run at first time(run `npx @deepseek-ai/dsh web`, then copy the output to DSH_URL):
|
|
5
|
+
```bash
|
|
6
|
+
export DSH_URL='http://127.0.0.1:3080/?token=<token>' && npx @itookit/dsht
|
|
7
|
+
```
|
|
8
|
+
Then every time only run (make sure `npx @deepseek-ai/dsh web` has run):
|
|
9
|
+
```bash
|
|
10
|
+
npx @itookit/dsht
|
|
11
|
+
```
|
|
4
12
|
|
|
5
13
|

|
|
6
14
|
|
|
@@ -168,6 +176,8 @@ Both paths read the same `DSH_URL` and `DSH_TOKEN` variables.
|
|
|
168
176
|
|
|
169
177
|
Select a workspace with ↑/↓ and Enter, then select a session or **New session**. **All sessions** also exposes sessions outside registered workspaces. **Add workspace (this directory)** registers the directory `dsht` itself runs in, and appears only while the host does not already have it; **Add workspace (host directory)** takes an existing absolute directory on the host, which may differ from your local filesystem, and Esc leaves that prompt for the picker again. Starting inside a registered workspace directory selects that workspace instead of showing the picker, with `←` in the session list switching to another; a session id passed on the command line still opens directly. Creating a session requires a selected workspace.
|
|
170
178
|
|
|
179
|
+
While the host is unreachable, the startup pickers show what is wrong and how to fix it instead of an empty list. A first attempt still in flight is named as connecting; an unreachable host prints the `npx @deepseek-ai/dsh web` command that starts it together with the first-run `DSH_URL` line; a refused credential asks for that URL or `DSH_TOKEN`. The raw transport failure stays underneath as a dim diagnostic, and the client keeps retrying on its own.
|
|
180
|
+
|
|
171
181
|
On first login, authentication exchanges the token at `GET /` and saves the cookie per HTTP origin. Later starts, including list commands, reuse that cookie without a token. The store uses `$XDG_STATE_HOME/dsht/auth`, or `~/.local/state/dsht/auth` when unset; `--auth-dir` or `DSHT_AUTH_DIR` overrides it. POSIX directories use 0700 and cookie files use 0600; Windows uses the account directory's inherited access controls. Launch tokens are never saved.
|
|
172
182
|
|
|
173
183
|
The host determines cookie expiration. An expired or rejected cookie requires a token again; a supplied token refreshes authentication automatically after HTTP 401. Network failures and HTTP 403 do not trigger token exchange. Corrupt or insecure cookie files fail explicitly. The base URL must be an origin without a path or extra query parameters, and the host must allow its hostname.
|
package/README.zh.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 中文
|
|
4
4
|
|
|
5
|
+
第一次运行时,需要使用 DSH_URL 传入 `npx @deepseek-ai/dsh web` 启动时打印的 url:
|
|
6
|
+
```bash
|
|
7
|
+
export DSH_URL='http://127.0.0.1:3080/?token=<token>' && npx @itookit/dsht
|
|
8
|
+
```
|
|
9
|
+
以后运行只需要执行(当然必须确认`npx @deepseek-ai/dsh web` 已经启动):
|
|
10
|
+
```bash
|
|
11
|
+
npx @itookit/dsht
|
|
12
|
+
```
|
|
5
13
|

|
|
6
14
|
|
|
7
15
|
> **dsht — 只要有终端,就能随时控制 DeepSeek Harness。**
|
|
@@ -168,6 +176,8 @@ npm start
|
|
|
168
176
|
|
|
169
177
|
使用 ↑/↓ 和 Enter 选择工作区,然后选择已有会话或 **New session**。**All sessions** 同时显示未归属注册工作区的会话。**Add workspace (this directory)** 直接注册 `dsht` 自身所在的目录,只在服务端尚未注册它时出现;**Add workspace (host directory)** 接收服务端已有目录的绝对路径,该路径可能与本机文件系统不同,按 Esc 可以退回选择器。在已注册的工作区目录里启动时,直接选中该工作区而不再显示选择器(会话列表里的 `←` 仍可切到别的工作区);命令行给出会话 ID 时依旧直接打开。新建会话前必须选择工作区。
|
|
170
178
|
|
|
179
|
+
宿主不可达时,启动选择器显示的是原因与解决办法,而不是一个空列表:尚在进行的首次尝试写成"连接中";连不上宿主时给出启动它的 `npx @deepseek-ai/dsh web` 命令与首次运行所需的 `DSH_URL` 行;凭据被拒时要求该 URL 或 `DSH_TOKEN`。原始传输错误以暗色诊断信息留在下方,客户端自行持续重连。
|
|
180
|
+
|
|
171
181
|
首次登录通过 `GET /` 兑换 token,并按 HTTP origin 保存 cookie。后续启动和列表命令自动复用 cookie,无需再次提供 token。默认目录为 `$XDG_STATE_HOME/dsht/auth`,未设置时使用 `~/.local/state/dsht/auth`;可通过 `--auth-dir` 或 `DSHT_AUTH_DIR` 覆盖。POSIX 下目录权限为 0700、cookie 文件为 0600;Windows 使用账户目录继承的访问控制。启动 token 永不保存。
|
|
172
182
|
|
|
173
183
|
Cookie 有效期由服务端决定。过期或被拒绝后,需要再次提供 token;已提供 token 时,HTTP 401 会自动触发重新认证。网络故障和 HTTP 403 不触发 token 兑换。损坏或权限不安全的 cookie 文件会明确报错。服务地址必须是不带路径、且除 `token` 外无其他查询参数的 origin,主机名须受服务端信任。
|
package/dist/ui/app.js
CHANGED
|
@@ -21,7 +21,8 @@ import { plainRows } from "./chat/shell-view.js";
|
|
|
21
21
|
import { useHistoryView } from "./chat/use-history-view.js";
|
|
22
22
|
import { Frozen } from "./frozen.js";
|
|
23
23
|
import { CopyMode } from "./copy-mode.js";
|
|
24
|
-
import { HelpPanel, HistoryDialog, ModelDialog, PickerScreen, PromptsDialog, QueueDialog, QueuedPreview, RemovalDialog, SearchResultsDialog, ThoughtsDialog } from "./dialogs/index.js";
|
|
24
|
+
import { HelpPanel, HistoryDialog, ModelDialog, OfflinePanel, PickerScreen, PromptsDialog, QueueDialog, QueuedPreview, RemovalDialog, SearchResultsDialog, ThoughtsDialog } from "./dialogs/index.js";
|
|
25
|
+
import { offlineGuidance } from "./offline.js";
|
|
25
26
|
import { LoopDialog, LoopMenu } from "./dialogs/loop.js";
|
|
26
27
|
import { usePanels } from "./dialogs/use-panels.js";
|
|
27
28
|
import { PeekPanel } from "./dialogs/peek.js";
|
|
@@ -882,6 +883,12 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
882
883
|
const peekPlainRows = useMemo(() => peek?.lines === undefined ? []
|
|
883
884
|
: peek.lines.flatMap(line => plainRows(line, width, 'shell')), [peek?.lines, width]);
|
|
884
885
|
const statusNotice = !['Connected', 'Idle', 'Running…', 'Responding…'].includes(state.status);
|
|
886
|
+
// A startup picker has nothing to list while the host is out of reach, so it carries the guidance
|
|
887
|
+
// in place of an empty list; every other screen keeps the status and failure lines instead.
|
|
888
|
+
const pickerScreen = state.screen === 'workspaces' || state.screen === 'sessions';
|
|
889
|
+
const offline = pickerScreen && !state.online
|
|
890
|
+
? offlineGuidance({ status: state.status, base: controller.base, lastFailure: state.lastFailure })
|
|
891
|
+
: undefined;
|
|
885
892
|
async function openSearchSession(sessionId, query) {
|
|
886
893
|
await historyOperation(async (signal) => {
|
|
887
894
|
if (!await controller.actions.selectSession(sessionId))
|
|
@@ -1038,13 +1045,14 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
1038
1045
|
missing: ledger.missing(),
|
|
1039
1046
|
};
|
|
1040
1047
|
})();
|
|
1041
|
-
return _jsx(ThemeContext.Provider, { value: theme, children: _jsx(CopyMode.Provider, { value: copyMode, children: _jsx(Frozen, { frozen: copyMode, identity: `${width}:${stdout.rows}`, children: _jsxs(Box, { flexDirection: "column", paddingX: 1, height: Math.max(1, (stdout.rows ?? 30) - 1), overflowY: "hidden", children: [copyMode && _jsx(Text, { color: theme.accent, children: "Copy mode \u00B7 drag to select \u00B7 Esc / Ctrl+S resumes" }), _jsx(Box, { ref: headerSection, flexShrink: 0, children: _jsx(ChatHeader, { title: headerTitle, mode: controller.queries.sessionMode, width: width, frozen: displayPaused, identity: `${width}:${state.sessionId}` }) }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, overflowY: "hidden", children: [_jsxs(Box, { ref: bodyPrefix, flexDirection: "column", flexShrink: 0, children: [foreground && _jsxs(Text, { dimColor: true, children: [foreground.label, " \u00B7 Esc / Ctrl+C cancel"] }), _jsx(Frozen, { frozen: statusPaused, identity: state.sessionId ?? "", children: statusNotice && _jsx(Text, { dimColor: true, wrap: "truncate-end", children: safeText(state.status) }) }), state.lastFailure && _jsx(Text, { color: theme.colors.error, children: state.lastFailure })] }), state.screen === 'chat' && (peek === undefined ? _jsx(ChatViewport, { rows: visible, showHistoryHint: showHistoryHint, dialogOpen: dialogOpen, historyWindow: !!historyWindow, frozen: displayPaused, identity: `${width}:${state.sessionId}:${position}:${pageSize}`, boxRef: conversationBox })
|
|
1048
|
+
return _jsx(ThemeContext.Provider, { value: theme, children: _jsx(CopyMode.Provider, { value: copyMode, children: _jsx(Frozen, { frozen: copyMode, identity: `${width}:${stdout.rows}`, children: _jsxs(Box, { flexDirection: "column", paddingX: 1, height: Math.max(1, (stdout.rows ?? 30) - 1), overflowY: "hidden", children: [copyMode && _jsx(Text, { color: theme.accent, children: "Copy mode \u00B7 drag to select \u00B7 Esc / Ctrl+S resumes" }), _jsx(Box, { ref: headerSection, flexShrink: 0, children: _jsx(ChatHeader, { title: headerTitle, mode: controller.queries.sessionMode, width: width, frozen: displayPaused, identity: `${width}:${state.sessionId}` }) }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, overflowY: "hidden", children: [_jsxs(Box, { ref: bodyPrefix, flexDirection: "column", flexShrink: 0, children: [foreground && _jsxs(Text, { dimColor: true, children: [foreground.label, " \u00B7 Esc / Ctrl+C cancel"] }), _jsx(Frozen, { frozen: statusPaused, identity: state.sessionId ?? "", children: statusNotice && offline === undefined && _jsx(Text, { dimColor: true, wrap: "truncate-end", children: safeText(state.status) }) }), state.lastFailure && offline === undefined && _jsx(Text, { color: theme.colors.error, children: state.lastFailure })] }), state.screen === 'chat' && (peek === undefined ? _jsx(ChatViewport, { rows: visible, showHistoryHint: showHistoryHint, dialogOpen: dialogOpen, historyWindow: !!historyWindow, frozen: displayPaused, identity: `${width}:${state.sessionId}:${position}:${pageSize}`, boxRef: conversationBox })
|
|
1042
1049
|
: _jsx(PeekPanel, { source: peek.source, ...(peek.error === undefined ? {} : { error: peek.error }), rows: peekVisible, position: peekPosition, total: peekTotal, width: width, now: Date.now(), boxRef: peekBox }))] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [notice && _jsx(Text, { dimColor: true, children: safeText(notice) }), composerIntent && _jsx(Text, { color: theme.colors.context, children: composerIntent.hint }), loop && _jsx(LoopStatus, { progress: loop }), peek === undefined && _jsxs(Box, { borderStyle: "round", borderColor: pending ? theme.colors.context : state.online ? theme.accent : theme.border, paddingX: 1, flexDirection: "column", flexShrink: 1, minHeight: 3, children: [_jsx(Box, { flexDirection: "column", flexShrink: 1, minHeight: 0, overflowY: "hidden", children: loopForm && loopRecord ? _jsx(LoopDialog, { record: loopRecord, enabled: state.online && controller.queries.foreground === undefined, onStart: run => startLoopFromForm(loopForm.name, run), onBack: () => { setLoopForm(undefined); setInput('/loop '); setDismissedLoopMenu(undefined); }, onClose: () => setLoopForm(undefined) }, loopForm.name) : queueOpen && !pending ? _jsx(QueueDialog, { queued: queued, rows: stdout.rows ?? 30, width: width, unavailable: !!state.controlError, enabled: !input && foreground === undefined && state.online, canSelect: () => !input && controller.queries.foreground === undefined && controller.state.online && !controller.state.pending.length, onRemove: id => operate(() => controller.actions.removeQueued(id)) }) : removal ? _jsx(RemovalDialog, { removal: removal, enabled: !input && foreground === undefined && state.online, canSelect: () => !input && controller.queries.foreground === undefined && controller.state.online, onCancel: () => setRemoval(undefined), onConfirm: () => operate(async () => { if (await controller.actions.removeTarget(removal))
|
|
1043
1050
|
setRemoval(undefined); }) }) : models ? _jsx(ModelDialog, { models: models, rows: stdout.rows ?? 30, width: width, enabled: !input && foreground === undefined && state.online, canSelect: () => !input && controller.queries.foreground === undefined && controller.state.online, onChoose: (provider, model, effort) => operate(async () => { if (await controller.actions.selectModel(provider, model, effort))
|
|
1044
|
-
setModelPanel(undefined); }), onOpen: (provider, model) => setModelPanel({ catalog: models.catalog, provider, model }), onBack: () => setModelPanel({ catalog: models.catalog }), onClose: () => setModelPanel(undefined) }) : searchResults ? _jsx(SearchResultsDialog, { query: searchResults.query, items: searchResults.items, hasMore: searchResults.hasMore, width: width, enabled: !input && foreground === undefined, canSelect: () => !input && controller.queries.foreground === undefined, onOpen: sessionId => operate(() => openSearchSession(sessionId, searchResults.query)), onClose: () => setSearchPanel(undefined) }) :
|
|
1051
|
+
setModelPanel(undefined); }), onOpen: (provider, model) => setModelPanel({ catalog: models.catalog, provider, model }), onBack: () => setModelPanel({ catalog: models.catalog }), onClose: () => setModelPanel(undefined) }) : searchResults ? _jsx(SearchResultsDialog, { query: searchResults.query, items: searchResults.items, hasMore: searchResults.hasMore, width: width, enabled: !input && foreground === undefined, canSelect: () => !input && controller.queries.foreground === undefined, onOpen: sessionId => operate(() => openSearchSession(sessionId, searchResults.query)), onClose: () => setSearchPanel(undefined) }) : pickerScreen ? offline === undefined ? _jsx(PickerScreen, { title: state.screen === 'workspaces' ? 'Choose workspace' : state.showAllSessions ? 'Choose session · All workspaces' : 'Choose session', identity: `${state.screen}:${state.workspaceId ?? ''}`, choices: choices, width: pickerWidth,
|
|
1045
1052
|
// A rollup of badges is read through the key beside it; spelled-out states need no key, and a
|
|
1046
1053
|
// terminal too narrow for the whole key gets the badges alone rather than half a legend.
|
|
1047
|
-
legend: state.screen === 'workspaces' && rollupStyle === 'badges' && pickerWidth >= 40 ? ROLLUP_LEGEND : undefined, enabled: state.online && foreground === undefined && !input, canSelect: () => !input && controller.state.online && controller.queries.foreground === undefined })
|
|
1054
|
+
legend: state.screen === 'workspaces' && rollupStyle === 'badges' && pickerWidth >= 40 ? ROLLUP_LEGEND : undefined, enabled: state.online && foreground === undefined && !input, canSelect: () => !input && controller.state.online && controller.queries.foreground === undefined })
|
|
1055
|
+
: _jsx(OfflinePanel, { guidance: offline }) : _jsxs(_Fragment, { children: [thoughtList && _jsx(ThoughtsDialog, { identity: `thoughts:${state.sessionId}`, options: thoughtOptions, empty: !thoughtEntries?.length && !liveThought, rows: stdout.rows ?? 30, enabled: !input && foreground === undefined, canSelect: () => !input && controller.queries.foreground === undefined }), promptsOpen && state.screen === 'chat' && _jsx(PromptsDialog, { identity: "prompts", prompts: controller.queries.prompts, error: controller.queries.promptsError, width: width, enabled: !input && foreground === undefined, canSelect: () => !input && controller.queries.foreground === undefined, onChoose: text => { setInput(text); openPrompts(false); }, onEdit: prompt => {
|
|
1048
1056
|
setComposerIntent({
|
|
1049
1057
|
hint: 'Editing saved prompt · Enter saves · Esc cancels',
|
|
1050
1058
|
emptyNotice: 'Type the prompt text; Enter then saves it',
|
|
@@ -2,6 +2,7 @@ import { type ObjectValue } from '../../json.ts';
|
|
|
2
2
|
import { type Message, type SavedPrompt } from '../../contracts.ts';
|
|
3
3
|
import type { HistorySearch, RemovalTarget } from '../../contracts.ts';
|
|
4
4
|
import type { QueuedInput } from '../../contracts.ts';
|
|
5
|
+
import type { OfflineGuidance } from '../offline.ts';
|
|
5
6
|
import { type Choice } from './picker.tsx';
|
|
6
7
|
/** Pending host input with explicit removal.
|
|
7
8
|
* @param props - Queue contents, geometry and availability.
|
|
@@ -91,6 +92,17 @@ export declare function PickerScreen({ title, identity, choices, enabled, canSel
|
|
|
91
92
|
/** Marker key shown above the list, for a rollup too compact to spell its states out. */
|
|
92
93
|
legend?: string;
|
|
93
94
|
}): import("react").JSX.Element;
|
|
95
|
+
/** What the startup picker shows while the host is unreachable.
|
|
96
|
+
*
|
|
97
|
+
* An empty list with a raw socket error beside it answers neither "what is wrong" nor "what do I do",
|
|
98
|
+
* so the picker is replaced by the state and the one command that resolves it. This is a startup
|
|
99
|
+
* surface only: a conversation keeps its transcript and lets the status bar report the loss.
|
|
100
|
+
* @param props - Guidance block built from the connection's published state.
|
|
101
|
+
* @returns The offline guidance panel.
|
|
102
|
+
*/
|
|
103
|
+
export declare function OfflinePanel({ guidance }: {
|
|
104
|
+
guidance: OfflineGuidance;
|
|
105
|
+
}): import("react").JSX.Element;
|
|
94
106
|
/** Newest-first reasoning summaries with the preceding user prompt.
|
|
95
107
|
* @param props - Loaded entries and lazy paging action.
|
|
96
108
|
* @returns The reasoning list.
|
package/dist/ui/dialogs/index.js
CHANGED
|
@@ -86,6 +86,18 @@ export function SearchResultsDialog({ query, items, hasMore, width, enabled, can
|
|
|
86
86
|
export function PickerScreen({ title, identity, choices, enabled, canSelect, width, legend }) {
|
|
87
87
|
return _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { bold: true, children: title }), legend !== undefined && _jsx(Text, { dimColor: true, wrap: "truncate-end", children: legend }), _jsx(Picker, { choices: choices, enabled: enabled, canSelect: canSelect, width: width }, identity)] });
|
|
88
88
|
}
|
|
89
|
+
/** What the startup picker shows while the host is unreachable.
|
|
90
|
+
*
|
|
91
|
+
* An empty list with a raw socket error beside it answers neither "what is wrong" nor "what do I do",
|
|
92
|
+
* so the picker is replaced by the state and the one command that resolves it. This is a startup
|
|
93
|
+
* surface only: a conversation keeps its transcript and lets the status bar report the loss.
|
|
94
|
+
* @param props - Guidance block built from the connection's published state.
|
|
95
|
+
* @returns The offline guidance panel.
|
|
96
|
+
*/
|
|
97
|
+
export function OfflinePanel({ guidance }) {
|
|
98
|
+
const theme = useTheme();
|
|
99
|
+
return _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { bold: true, color: theme.status.offline, wrap: "truncate-end", children: safeText(guidance.title) }), guidance.lines.map((line, index) => _jsx(Text, { wrap: "truncate-end", color: line.startsWith(' ') ? theme.colors.tool : undefined, children: safeText(line) }, index)), guidance.detail !== undefined && _jsx(Text, { dimColor: true, wrap: "truncate-end", children: safeText(guidance.detail) })] });
|
|
100
|
+
}
|
|
89
101
|
/** Newest-first reasoning summaries with the preceding user prompt.
|
|
90
102
|
* @param props - Loaded entries and lazy paging action.
|
|
91
103
|
* @returns The reasoning list.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** What the startup picker shows when the host it needs is not answering.
|
|
2
|
+
*
|
|
3
|
+
* `dsht` deliberately never starts Harness, so an unreachable host is an operator problem with a
|
|
4
|
+
* known fix. This module turns the connection's published status into a short, accurate block: the
|
|
5
|
+
* state first, then the one command that resolves it. It is pure text, so the wording is testable on
|
|
6
|
+
* its own and the rendering belongs to `dialogs/`.
|
|
7
|
+
*/
|
|
8
|
+
/** The command that starts the host this client talks to. */
|
|
9
|
+
export declare const HOST_COMMAND = "npx @deepseek-ai/dsh web";
|
|
10
|
+
/** What the startup picker shows while the host cannot be reached. */
|
|
11
|
+
export interface OfflineGuidance {
|
|
12
|
+
/** Which host, and which of its three states the connection published. */
|
|
13
|
+
readonly title: string;
|
|
14
|
+
/** What happened and what to do about it, in reading order. */
|
|
15
|
+
readonly lines: readonly string[];
|
|
16
|
+
/** The raw transport failure, rendered dim under the guidance rather than as the message. */
|
|
17
|
+
readonly detail?: string;
|
|
18
|
+
}
|
|
19
|
+
/** The `DSH_URL` export that carries a first-run token for one host.
|
|
20
|
+
*
|
|
21
|
+
* `dsh web` prints its URL with a `token` parameter, and that printed line is what a first run
|
|
22
|
+
* exports unchanged; a host URL that already carries query parameters keeps its own separator.
|
|
23
|
+
* @param base - Host URL this client targets.
|
|
24
|
+
* @returns One `export DSH_URL=…` line.
|
|
25
|
+
*/
|
|
26
|
+
export declare function dshUrlLine(base: string): string;
|
|
27
|
+
/** Build the startup guidance for one connection state.
|
|
28
|
+
*
|
|
29
|
+
* The first attempt is named as connecting rather than as offline: a client that has not finished
|
|
30
|
+
* trying yet has not learned that anything is wrong, and saying "offline" then would be a guess.
|
|
31
|
+
* @param source - Published connection status, host URL and last failure.
|
|
32
|
+
* @returns The guidance block; never empty, because the caller renders it only while offline.
|
|
33
|
+
*/
|
|
34
|
+
export declare function offlineGuidance(source: {
|
|
35
|
+
status: string;
|
|
36
|
+
base: string;
|
|
37
|
+
lastFailure?: string;
|
|
38
|
+
}): OfflineGuidance;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** What the startup picker shows when the host it needs is not answering.
|
|
2
|
+
*
|
|
3
|
+
* `dsht` deliberately never starts Harness, so an unreachable host is an operator problem with a
|
|
4
|
+
* known fix. This module turns the connection's published status into a short, accurate block: the
|
|
5
|
+
* state first, then the one command that resolves it. It is pure text, so the wording is testable on
|
|
6
|
+
* its own and the rendering belongs to `dialogs/`.
|
|
7
|
+
*/
|
|
8
|
+
/** The command that starts the host this client talks to. */
|
|
9
|
+
export const HOST_COMMAND = 'npx @deepseek-ai/dsh web';
|
|
10
|
+
/** The `DSH_URL` export that carries a first-run token for one host.
|
|
11
|
+
*
|
|
12
|
+
* `dsh web` prints its URL with a `token` parameter, and that printed line is what a first run
|
|
13
|
+
* exports unchanged; a host URL that already carries query parameters keeps its own separator.
|
|
14
|
+
* @param base - Host URL this client targets.
|
|
15
|
+
* @returns One `export DSH_URL=…` line.
|
|
16
|
+
*/
|
|
17
|
+
export function dshUrlLine(base) {
|
|
18
|
+
const separator = base.includes('?') ? '&' : '?';
|
|
19
|
+
const root = base.includes('?') || base.endsWith('/') ? base : `${base}/`;
|
|
20
|
+
return `export DSH_URL='${root}${separator}token=<token>'`;
|
|
21
|
+
}
|
|
22
|
+
/** Build the startup guidance for one connection state.
|
|
23
|
+
*
|
|
24
|
+
* The first attempt is named as connecting rather than as offline: a client that has not finished
|
|
25
|
+
* trying yet has not learned that anything is wrong, and saying "offline" then would be a guess.
|
|
26
|
+
* @param source - Published connection status, host URL and last failure.
|
|
27
|
+
* @returns The guidance block; never empty, because the caller renders it only while offline.
|
|
28
|
+
*/
|
|
29
|
+
export function offlineGuidance(source) {
|
|
30
|
+
const { base, status } = source;
|
|
31
|
+
const detail = source.lastFailure === undefined || source.lastFailure === '' ? undefined : source.lastFailure;
|
|
32
|
+
const url = dshUrlLine(base);
|
|
33
|
+
if (status === 'Login required') {
|
|
34
|
+
return { title: `Login required · ${base}`, lines: [
|
|
35
|
+
'No token was given, or the saved cookie no longer works.',
|
|
36
|
+
'Start the host and export the URL it prints, then run dsht again:',
|
|
37
|
+
` ${url}`,
|
|
38
|
+
'Or export DSH_TOKEN. Tokens are never saved.',
|
|
39
|
+
], ...(detail === undefined ? {} : { detail }) };
|
|
40
|
+
}
|
|
41
|
+
if (status === 'Connecting…') {
|
|
42
|
+
return { title: `Connecting… · ${base}`, lines: [
|
|
43
|
+
'Waiting for DeepSeek Harness at that address.',
|
|
44
|
+
'If nothing is listening yet, start it there:',
|
|
45
|
+
` ${HOST_COMMAND}`,
|
|
46
|
+
'First run only, point dsht at the URL it prints:',
|
|
47
|
+
` ${url}`,
|
|
48
|
+
], ...(detail === undefined ? {} : { detail }) };
|
|
49
|
+
}
|
|
50
|
+
return { title: `Host offline · ${base}`, lines: [
|
|
51
|
+
'DeepSeek Harness is not answering yet.',
|
|
52
|
+
'Start it on that host; this screen reconnects by itself:',
|
|
53
|
+
` ${HOST_COMMAND}`,
|
|
54
|
+
'First run only, point dsht at the URL it prints:',
|
|
55
|
+
` ${url}`,
|
|
56
|
+
], ...(detail === undefined ? {} : { detail }) };
|
|
57
|
+
}
|