@aiwayds/dsh-tui-pi 2.33.0 → 2.34.0
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.md +2 -0
- package/bin/boot.mjs +15 -1
- package/bin/host-guard.mjs +261 -0
- package/bin/onboard-core.mjs +7 -4
- package/lib/index.js +21 -2
- package/lib/index.js.map +1 -1
- package/lib/session.d.ts +36 -0
- package/lib/session.js +129 -0
- package/lib/session.js.map +1 -1
- package/lib/theme-settings.d.ts +4 -0
- package/lib/theme-settings.js +9 -0
- package/lib/theme-settings.js.map +1 -1
- package/locales/en.json +3 -1
- package/locales/ja.json +3 -1
- package/locales/ko.json +3 -1
- package/locales/zh-CN.json +3 -1
- package/package.json +1 -1
- package/skills/dsh-tui-pi-config/SKILL.md +3 -1
- package/lib/profile.d.ts +0 -82
- package/lib/profile.js +0 -665
- package/lib/profile.js.map +0 -1
package/README.md
CHANGED
|
@@ -53,6 +53,8 @@ dsh plugin --profile tui add @aiwayds/dsh-tui-pi
|
|
|
53
53
|
dsh --profile tui # launch
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
Never add `@deepseek-ai/*` packages to a profile by hand — the host provides them, and their npm `latest` tag is a 0.0.1 stub (a bare-name `pnpm add @deepseek-ai/dsh-llm` lands that stub next to the plugin and shadows the host's copy; the symptom is the loader's `does not provide an export named …` and the plugin not activating). The `dsh-tui-pi` launcher checks for exactly this before every boot and, when it finds a shadowing copy, stops with both paths, both versions and the removal commands instead of booting into the broken state.
|
|
57
|
+
|
|
56
58
|
Legacy `session_projcache` records (written before dsh 0.1.2-alpha.4) are migrated automatically at profile load — idempotent, with per-file backups, never blocking startup; the `dsh-tui-pi` launcher runs the same preflight.
|
|
57
59
|
|
|
58
60
|
Everything that used to need manual patching — the canvas background, the `@deepseek-ai` module closure, the compaction backend — now happens automatically. For a normal install, upgrading the global package upgrades the profile on the next launch (see above). `dev-upgrade` remains for dev machines that install the profile from a checked-out repo:
|
package/bin/boot.mjs
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
* missing any of the three pieces (dsh host, pnpm, mounted tui profile).
|
|
6
6
|
*
|
|
7
7
|
* Fast path — host present and profile current: no output, straight to the
|
|
8
|
-
* TUI, exactly like the plain `dsh --profile tui` this replaces.
|
|
8
|
+
* TUI, exactly like the plain `dsh --profile tui` this replaces. One check
|
|
9
|
+
* runs first (bin/host-guard.mjs): the mounted plugin must resolve the host
|
|
10
|
+
* packages it imports to the running host's own copies — a stray copy inside
|
|
11
|
+
* the profile would otherwise die at loader time with an unactionable
|
|
12
|
+
* SyntaxError, so the guard stops with the paths and the removal commands.
|
|
9
13
|
*
|
|
10
14
|
* Version follow: after `npm i -g` upgrades this package, the profile still
|
|
11
15
|
* holds the old copy; the next launch (or the package's postinstall) re-runs
|
|
@@ -16,10 +20,13 @@
|
|
|
16
20
|
*/
|
|
17
21
|
|
|
18
22
|
import { spawnSync } from 'node:child_process'
|
|
23
|
+
import { checkHostPackages } from './host-guard.mjs'
|
|
19
24
|
import {
|
|
20
25
|
dshBinPath,
|
|
26
|
+
dshHome,
|
|
21
27
|
profileBehind,
|
|
22
28
|
profileMounted,
|
|
29
|
+
profilePluginDir,
|
|
23
30
|
runOnboard,
|
|
24
31
|
} from './onboard-core.mjs'
|
|
25
32
|
|
|
@@ -33,6 +40,13 @@ function boot(dsh) {
|
|
|
33
40
|
const dsh = dshBinPath()
|
|
34
41
|
|
|
35
42
|
if (dsh && profileMounted() && !profileBehind()) {
|
|
43
|
+
const guard = checkHostPackages({
|
|
44
|
+
pluginDir: profilePluginDir(),
|
|
45
|
+
dshBin: dsh,
|
|
46
|
+
dshHome: dshHome(),
|
|
47
|
+
log: (line) => console.error(line),
|
|
48
|
+
})
|
|
49
|
+
if (!guard.ok) process.exit(1)
|
|
36
50
|
boot(dsh)
|
|
37
51
|
}
|
|
38
52
|
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fast-path guard for the launcher (bin/boot.mjs): the plugin mounted in the
|
|
3
|
+
* profile must resolve every `@deepseek-ai/*` package it imports to the same
|
|
4
|
+
* copy the running host ships.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists: plugins live in `~/.dsh/profiles/<name>/node_modules` and
|
|
7
|
+
* resolve host packages by walking up to the host's shared closure tree
|
|
8
|
+
* (`~/.dsh/profiles/node_modules`, links into the global host install). Any
|
|
9
|
+
* stray `@deepseek-ai/*` copy sitting closer to the plugin — a bare-name
|
|
10
|
+
* `pnpm add @deepseek-ai/dsh-llm` is the classic — shadows the host's copy.
|
|
11
|
+
* The failure then surfaces as a raw loader SyntaxError ("does not provide
|
|
12
|
+
* an export named …") that hides both the version conflict and its fix.
|
|
13
|
+
*
|
|
14
|
+
* Official dsh packages keep their npm `latest` dist-tag at a 0.0.1 stub
|
|
15
|
+
* (as of 2026-10: `@deepseek-ai/dsh-llm@latest` = 0.0.1-rc.1), so the stub
|
|
16
|
+
* lacks APIs introduced in the 0.1.3+ line. This guard turns that state
|
|
17
|
+
* into a plain-language stop with the removal commands, before `dsh` boots.
|
|
18
|
+
*
|
|
19
|
+
* Contract:
|
|
20
|
+
* - healthy profile: no output, returns { ok: true } — the fast path
|
|
21
|
+
* stays silent;
|
|
22
|
+
* - a profile-resident copy older than the host's: fatal stop with the
|
|
23
|
+
* two paths, the versions, and the exact removal commands;
|
|
24
|
+
* - ambiguous states (dev `link:` mounts, copies newer than the host,
|
|
25
|
+
* an unverifiable host side): one stderr line, boot continues — the
|
|
26
|
+
* guard never blocks a working setup on a guess.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { createRequire } from 'node:module'
|
|
30
|
+
import { existsSync, readFileSync, readlinkSync, realpathSync } from 'node:fs'
|
|
31
|
+
import { dirname, isAbsolute, join, sep } from 'node:path'
|
|
32
|
+
import { compareVersions } from './onboard-core.mjs'
|
|
33
|
+
|
|
34
|
+
// Every `@deepseek-ai/*` package the compiled plugin imports at runtime
|
|
35
|
+
// (type-only imports in src/ are erased by tsc and do not count). Kept in
|
|
36
|
+
// lockstep with lib/ by test/host-guard.test.mjs; a drifted list fails CI.
|
|
37
|
+
export const HOST_PACKAGES = [
|
|
38
|
+
'@deepseek-ai/dsh-agent',
|
|
39
|
+
'@deepseek-ai/dsh-attachment-local',
|
|
40
|
+
'@deepseek-ai/dsh-commands',
|
|
41
|
+
'@deepseek-ai/dsh-llm',
|
|
42
|
+
'@deepseek-ai/dsh-permission-presets',
|
|
43
|
+
'@deepseek-ai/dsh-session',
|
|
44
|
+
'@deepseek-ai/dsh-session-persistence',
|
|
45
|
+
'@deepseek-ai/dsh-session-projection-cache',
|
|
46
|
+
'@deepseek-ai/dsh-settings',
|
|
47
|
+
'@deepseek-ai/dsh-tools',
|
|
48
|
+
'@deepseek-ai/schemastery',
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
const HOST_NAME = '@deepseek-ai/dsh'
|
|
52
|
+
|
|
53
|
+
// Oldest host line this suite supports (the baked-in floor of
|
|
54
|
+
// onboard-core.mjs minHostVersion). Anything older inside a profile is not a
|
|
55
|
+
// release of anything — it is the npm `latest` stub.
|
|
56
|
+
const ANCIENT_FLOOR = '0.2.1-rc.2'
|
|
57
|
+
|
|
58
|
+
const PKG = '@aiwayds/dsh-tui-pi'
|
|
59
|
+
|
|
60
|
+
function readPackageAt(pkgPath) {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
63
|
+
} catch {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The nearest package root around a resolved file: version of the copy the
|
|
69
|
+
// plugin would actually load.
|
|
70
|
+
function packageInfoFromFile(file) {
|
|
71
|
+
let dir = dirname(file)
|
|
72
|
+
for (let i = 0; i < 8; i += 1) {
|
|
73
|
+
const pkg = readPackageAt(join(dir, 'package.json'))
|
|
74
|
+
if (pkg?.name) return { dir, version: pkg.version ?? null }
|
|
75
|
+
const parent = dirname(dir)
|
|
76
|
+
if (parent === dir) return null
|
|
77
|
+
dir = parent
|
|
78
|
+
}
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// The global host install root, from the `dsh` bin the launcher is about to
|
|
83
|
+
// spawn — `<root>/node_modules` is where its closure copies live.
|
|
84
|
+
export function hostRootFromBin(dshBin) {
|
|
85
|
+
try {
|
|
86
|
+
let dir = dirname(realpathSync(dshBin))
|
|
87
|
+
for (let i = 0; i < 6; i += 1) {
|
|
88
|
+
const pkg = readPackageAt(join(dir, 'package.json'))
|
|
89
|
+
if (pkg?.name === HOST_NAME) return dir
|
|
90
|
+
const parent = dirname(dir)
|
|
91
|
+
if (parent === dir) return null
|
|
92
|
+
dir = parent
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
/* not a file, or unreadable */
|
|
96
|
+
}
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// True when the mounted plugin is a symlink pointing outside the dsh home —
|
|
101
|
+
// the dev `link:` checkout layout, where the checkout's own closure copy is
|
|
102
|
+
// legitimate and a version delta is a warning, not a stop.
|
|
103
|
+
function isDevMount(pluginDir, dshHome) {
|
|
104
|
+
let target
|
|
105
|
+
try {
|
|
106
|
+
target = readlinkSync(pluginDir)
|
|
107
|
+
if (!target) return false
|
|
108
|
+
} catch {
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
const resolved = isAbsolute(target) ? target : join(dirname(pluginDir), target)
|
|
112
|
+
try {
|
|
113
|
+
return !realpathSync(resolved).startsWith(realpathSync(dshHome))
|
|
114
|
+
} catch {
|
|
115
|
+
return true
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resolveFrom(pluginDir, name) {
|
|
120
|
+
try {
|
|
121
|
+
const req = createRequire(join(pluginDir, 'package.json'))
|
|
122
|
+
return req.resolve(name)
|
|
123
|
+
} catch {
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Version the host intends plugins to see: the shared closure tree first
|
|
129
|
+
// (what walk-up resolution is supposed to land on), then the host's own
|
|
130
|
+
// node_modules. Null when neither can be read — the guard then fails open.
|
|
131
|
+
function intendedInfo(name, { sharedTree, hostRoot }) {
|
|
132
|
+
const candidates = []
|
|
133
|
+
if (sharedTree) candidates.push(join(sharedTree, name))
|
|
134
|
+
if (hostRoot) candidates.push(join(hostRoot, 'node_modules', name))
|
|
135
|
+
for (const dir of candidates) {
|
|
136
|
+
const pkg = readPackageAt(join(dir, 'package.json'))
|
|
137
|
+
if (pkg?.name) return { dir, version: pkg.version ?? null }
|
|
138
|
+
}
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// The install root a resolved copy lives in (`…/<root>/node_modules/<name>`),
|
|
143
|
+
// used to shape the removal commands.
|
|
144
|
+
function installRootOf(resolvedPath) {
|
|
145
|
+
const marker = `${sep}node_modules${sep}`
|
|
146
|
+
const idx = resolvedPath.lastIndexOf(marker)
|
|
147
|
+
return idx === -1 ? null : resolvedPath.slice(0, idx)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function realOr(path) {
|
|
151
|
+
try {
|
|
152
|
+
return realpathSync(path)
|
|
153
|
+
} catch {
|
|
154
|
+
return path
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function profileRemedy(name, resolvedPath, dshHome) {
|
|
159
|
+
const root = installRootOf(resolvedPath)
|
|
160
|
+
if (!root || !dshHome || !realOr(root).startsWith(realOr(dshHome))) {
|
|
161
|
+
return `remove the stray copy at ${resolvedPath}, then re-mount:\n dsh plugin --profile tui add ${PKG}`
|
|
162
|
+
}
|
|
163
|
+
const deps = readPackageAt(join(root, 'package.json'))?.dependencies ?? {}
|
|
164
|
+
const rm = name in deps
|
|
165
|
+
? `cd ${root} && pnpm remove ${name}`
|
|
166
|
+
: `rm -rf ${resolvedPath}`
|
|
167
|
+
return `${rm}\n dsh plugin --profile tui add ${PKG}`
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Check the mounted plugin's view of the host closure. Returns { ok, fatal,
|
|
172
|
+
* warnings }; prints nothing itself — pass a log to emit the diagnostics.
|
|
173
|
+
* All inputs optional except pluginDir; anything unresolvable degrades to
|
|
174
|
+
* warn, so an exotic install layout never blocks a working boot.
|
|
175
|
+
*/
|
|
176
|
+
export function checkHostPackages({ pluginDir, dshBin, dshHome, log = () => {} } = {}) {
|
|
177
|
+
const fatal = []
|
|
178
|
+
const warnings = []
|
|
179
|
+
if (!pluginDir || !existsSync(join(pluginDir, 'package.json'))) {
|
|
180
|
+
return { ok: true, fatal, warnings }
|
|
181
|
+
}
|
|
182
|
+
const home = dshHome ?? ''
|
|
183
|
+
const sharedTree = join(home, 'profiles', 'node_modules')
|
|
184
|
+
const hostRoot = dshBin ? hostRootFromBin(dshBin) : null
|
|
185
|
+
const dev = home ? isDevMount(pluginDir, home) : false
|
|
186
|
+
|
|
187
|
+
for (const name of HOST_PACKAGES) {
|
|
188
|
+
const resolvedFile = resolveFrom(pluginDir, name)
|
|
189
|
+
const resolved = resolvedFile ? packageInfoFromFile(resolvedFile) : null
|
|
190
|
+
const intended = intendedInfo(name, { sharedTree: home ? sharedTree : null, hostRoot })
|
|
191
|
+
|
|
192
|
+
if (!resolved) {
|
|
193
|
+
// The plugin statically imports every package in the list; nothing
|
|
194
|
+
// resolvable means the loader is about to die on it.
|
|
195
|
+
const finding = {
|
|
196
|
+
name,
|
|
197
|
+
kind: 'unresolved',
|
|
198
|
+
severity: dev ? 'warn' : 'fatal',
|
|
199
|
+
resolved: null,
|
|
200
|
+
intended,
|
|
201
|
+
}
|
|
202
|
+
;(finding.severity === 'fatal' ? fatal : warnings).push(finding)
|
|
203
|
+
continue
|
|
204
|
+
}
|
|
205
|
+
if (!intended?.version || !resolved.version) {
|
|
206
|
+
warnings.push({ name, kind: 'unverified', severity: 'warn', resolved, intended })
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
const cmp = compareVersions(resolved.version, intended.version)
|
|
210
|
+
if (cmp === 0) continue
|
|
211
|
+
if (cmp < 0) {
|
|
212
|
+
const finding = {
|
|
213
|
+
name,
|
|
214
|
+
kind: 'shadowed',
|
|
215
|
+
severity: dev ? 'warn' : 'fatal',
|
|
216
|
+
ancient: compareVersions(resolved.version, ANCIENT_FLOOR) < 0,
|
|
217
|
+
resolved,
|
|
218
|
+
intended,
|
|
219
|
+
}
|
|
220
|
+
;(finding.severity === 'fatal' ? fatal : warnings).push(finding)
|
|
221
|
+
} else {
|
|
222
|
+
warnings.push({ name, kind: 'newer', severity: 'warn', resolved, intended })
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (const f of warnings) {
|
|
227
|
+
if (f.kind === 'newer') {
|
|
228
|
+
log(`dsh-tui-pi: note — ${f.name} resolves to ${f.resolved.version} (${f.resolved.dir}), newer than the host's ${f.intended?.version ?? '(unreadable)'}; booting anyway.`)
|
|
229
|
+
} else if (f.kind === 'unverified') {
|
|
230
|
+
log(`dsh-tui-pi: note — ${f.name} resolves to ${f.resolved.version ?? '(unknown version)'} (${f.resolved.dir}); the host's own copy could not be read to compare.`)
|
|
231
|
+
} else if (f.kind === 'unresolved') {
|
|
232
|
+
log(`dsh-tui-pi: note — ${f.name} is not resolvable from the dev checkout; if the TUI fails to load it, run \`pnpm install\` in the checkout.`)
|
|
233
|
+
} else {
|
|
234
|
+
log(`dsh-tui-pi: dev mount — ${f.name} resolves to ${f.resolved.version} (${f.resolved.dir}), older than the host's ${f.intended?.version}; if the TUI fails to load, refresh the checkout's closure (\`pnpm install\`).`)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (fatal.length) {
|
|
239
|
+
log('dsh-tui-pi: cannot boot cleanly — the profile resolves a dsh host package to a copy that is not the running host\'s.')
|
|
240
|
+
for (const f of fatal) {
|
|
241
|
+
if (f.kind === 'unresolved') {
|
|
242
|
+
log(`\n ${f.name}\n not resolvable from the mounted plugin at all.`)
|
|
243
|
+
log(`\n Fix — re-mount the suite and boot once so the host links its closure:\n dsh plugin --profile tui add ${PKG}\n dsh-tui-pi`)
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
246
|
+
log(`\n ${f.name}`)
|
|
247
|
+
log(` the plugin resolves: ${f.resolved.version ?? '(unknown)'}`)
|
|
248
|
+
log(` ← ${f.resolved.dir}`)
|
|
249
|
+
log(` the host ships: ${f.intended?.version ?? '(unreadable)'}`)
|
|
250
|
+
log(` ← ${f.intended ? realOr(f.intended.dir) : '(shared closure link missing)'}`)
|
|
251
|
+
if (f.ancient) {
|
|
252
|
+
log('\n Versions before 0.2.1-rc.2 are stubs, not releases: official dsh packages keep their npm `latest` dist-tag at a 0.0.1 placeholder, so installing by bare package name (`pnpm add @deepseek-ai/dsh-llm`) lands a stub in the profile and breaks every plugin import. Install with an explicit version instead (e.g. @deepseek-ai/dsh-llm@0.2.1-rc.5).')
|
|
253
|
+
} else {
|
|
254
|
+
log('\n A stale copy inside the profile shadows the host\'s own.')
|
|
255
|
+
}
|
|
256
|
+
log(`\n Fix — ${profileRemedy(f.name, f.resolved.dir, home)}`)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return { ok: fatal.length === 0, fatal, warnings }
|
|
261
|
+
}
|
package/bin/onboard-core.mjs
CHANGED
|
@@ -46,16 +46,19 @@ export function dshHome() {
|
|
|
46
46
|
return process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// Where `dsh plugin add` mounts this package inside the tui profile.
|
|
50
|
+
export function profilePluginDir() {
|
|
51
|
+
return join(dshHome(), 'profiles', PROFILE, 'node_modules', PKG)
|
|
52
|
+
}
|
|
53
|
+
|
|
49
54
|
// The profile counts as set up once this package is resolvable inside it.
|
|
50
55
|
export function profileMounted() {
|
|
51
|
-
return existsSync(join(
|
|
56
|
+
return existsSync(join(profilePluginDir(), 'package.json'))
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
export function profileVersion() {
|
|
55
60
|
try {
|
|
56
|
-
return JSON.parse(
|
|
57
|
-
readFileSync(join(dshHome(), 'profiles', PROFILE, 'node_modules', PKG, 'package.json'), 'utf8'),
|
|
58
|
-
).version ?? null
|
|
61
|
+
return JSON.parse(readFileSync(join(profilePluginDir(), 'package.json'), 'utf8')).version ?? null
|
|
59
62
|
} catch {
|
|
60
63
|
return null
|
|
61
64
|
}
|
package/lib/index.js
CHANGED
|
@@ -115,7 +115,7 @@ const SKILL_RESOURCE_BASE = {
|
|
|
115
115
|
};
|
|
116
116
|
const SKILL_INVOCATION = { modelInvocable: true, userInvocable: true };
|
|
117
117
|
/** Routing description; must stay identical to the SKILL.md frontmatter (asserted in tests). */
|
|
118
|
-
const SKILL_DESCRIPTION = 'dsh TUI
|
|
118
|
+
const SKILL_DESCRIPTION = 'dsh TUI 增强套件配置指南。凡涉及 TUI 主题/面板/footer、界面语言、子代理并发与轮数限制、模型收藏与隐藏、会话保留与 /resume 过滤、ask_user 超时、空闲放锁、preset 记忆,或要配置 dsh-tui 段时先读本指南:entry `dsh-tui` 的 18 键(language/theme/panelHeight/maxAgents/maxRounds/maxRoundsGrace/disableSubagent/registeredOnly/footerHints/cacheHitMode/iconSet/rememberPreset/favoriteModels/hiddenModels/retention/resume/askUser/idleReleaseSec)、DSH_TUI_* 环境变量、快速上手、keybindings.json 与 /hotkeys。触发词:tui、主题、theme、面板、footer、language、收藏模型、隐藏模型、保留策略、resume、preset、idleReleaseSec。';
|
|
119
119
|
const SKILL_CANDIDATE = {
|
|
120
120
|
name: SKILL_PROVIDER_NAME,
|
|
121
121
|
description: SKILL_DESCRIPTION,
|
|
@@ -823,8 +823,27 @@ export function apply(ctx, config) {
|
|
|
823
823
|
emitNotice(t('tui.takeover.writeLock'));
|
|
824
824
|
ui.requestRender();
|
|
825
825
|
},
|
|
826
|
+
onReattached: async (idRaw) => {
|
|
827
|
+
// The idle-released handle won its lease back on the next prompt or
|
|
828
|
+
// command: rebind exactly like a manual /resume and rebuild the
|
|
829
|
+
// display so the turns other surfaces wrote while the lock was free
|
|
830
|
+
// appear.
|
|
831
|
+
btwController.cancelAll();
|
|
832
|
+
const resumed = await bridge.resume(SessionId(idRaw));
|
|
833
|
+
refreshPermissionPreset();
|
|
834
|
+
renderer.clear();
|
|
835
|
+
liveWidgets.clear();
|
|
836
|
+
const session = resumed.agent.session;
|
|
837
|
+
const adopted = 'adopted' in resumed && resumed.adopted === true;
|
|
838
|
+
// Raw-event replay (T14 note — see onRemotePromotable above).
|
|
839
|
+
bridge.replay(adopted ? session.snapshotEvents() : session.snapshotEvents().filter(event => event.seq < session.firstLiveSeq));
|
|
840
|
+
emitNotice(t('tui.idleRelease.reattached'));
|
|
841
|
+
ui.requestRender();
|
|
842
|
+
},
|
|
826
843
|
};
|
|
827
|
-
const bridge = new DshSessionBridge(ctx, bridgeCallbacksWithTakeover
|
|
844
|
+
const bridge = new DshSessionBridge(ctx, bridgeCallbacksWithTakeover, {
|
|
845
|
+
idleReleaseSec: () => config.idleReleaseSec.get(),
|
|
846
|
+
});
|
|
828
847
|
bridgeRef = bridge;
|
|
829
848
|
// Preset memory's creation-time leg: seed the remembered selection so the
|
|
830
849
|
// FIRST session creation (the lazy create on the first submitted prompt)
|