dsh-ssh-tui 0.8.4-rc.1 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -13
- package/README.en.md +4 -3
- package/docs/release-notes-0.8.4-rc.2.md +72 -0
- package/docs/release-notes-0.8.4.md +129 -0
- package/lib/doctor.js +41 -4
- package/lib/doctor.js.map +1 -1
- package/lib/i18n/en.js +6 -1
- package/lib/i18n/en.js.map +1 -1
- package/lib/i18n/zh.js +6 -1
- package/lib/i18n/zh.js.map +1 -1
- package/lib/preset-rows.js +70 -19
- package/lib/preset-rows.js.map +1 -1
- package/lib/terminal-input.js +13 -3
- package/lib/terminal-input.js.map +1 -1
- package/lib/tui.js +160 -15
- package/lib/tui.js.map +1 -1
- package/lib/types/doctor.d.ts +13 -1
- package/lib/types/preset-rows.d.ts +22 -7
- package/lib/types/tui.d.ts +32 -4
- package/package.json +1 -1
package/lib/tui.js
CHANGED
|
@@ -48,7 +48,7 @@ import { resolveTheme, setActiveTheme, themeByName, themeEmphasisToken, themeExt
|
|
|
48
48
|
import { presetLabel, profileFromArgv } from './preset-label.js';
|
|
49
49
|
import { flattenGroups, groupPresets, optionMatches } from './preset-picker.js';
|
|
50
50
|
import { isRefusal, planCopy, planDelete, planMetadata, presetDirectory, } from './preset-authoring.js';
|
|
51
|
-
import { ensureRosterRows, planDuplicateRepair, planRosterRepair,
|
|
51
|
+
import { ensureRosterRows, planDuplicateRepair, planRosterRepair, rosterPatchPath, rosterRows, writePatchWithBackup, ALL_ROSTER_ROWS, } from './preset-rows.js';
|
|
52
52
|
import { SessionStatsTracker, sessionTokenTotal, statsRowOf, } from './stats.js';
|
|
53
53
|
import { QUESTION_OPTION_KEYS, applyQuestionFilter, backspaceQuestionFilter, clearQuestionFilter, typeQuestionFilter, visibleQuestionIndexes, confirmAnswer, inspectClosesOn, moveQuestionCursor, optionsLength, questionOptionIndex, initialQuestionSelection, questionOptionMarker, questionSubmit, selectQuestionOptionByKey, windowInteractionLines, PICKER_ROLE, interactionRole, stallsTask, surfacePriority, isSurfaceDialog, } from './dialogs.js';
|
|
54
54
|
import { commandSuggestions, localizedCommands } from './commands.js';
|
|
@@ -2241,6 +2241,7 @@ export class SshTui {
|
|
|
2241
2241
|
const credentials = this.ctx.get('credentials');
|
|
2242
2242
|
const provider = this.currentProviderId();
|
|
2243
2243
|
if (providerUsesLocalOAuth(provider)) {
|
|
2244
|
+
await this.repairAgentPlaneAtBoot();
|
|
2244
2245
|
// Workspace feedback *about* setup — not the wizard's own surface, which is the
|
|
2245
2246
|
// setup Screen (B2.6 §12). These rows are the command plane's.
|
|
2246
2247
|
this.pushRow(represent('command-status', {
|
|
@@ -2276,6 +2277,7 @@ export class SshTui {
|
|
|
2276
2277
|
}
|
|
2277
2278
|
if (envKey !== undefined && envKey !== '') {
|
|
2278
2279
|
if (stored || existsSync(DSH_ENV_FILE) || this.resume) {
|
|
2280
|
+
await this.repairAgentPlaneAtBoot();
|
|
2279
2281
|
this.pushRow(represent('command-status', {
|
|
2280
2282
|
kind: 'system',
|
|
2281
2283
|
text: t('onboard.envInUse', { env: envRef }),
|
|
@@ -2301,8 +2303,10 @@ export class SshTui {
|
|
|
2301
2303
|
// got an empty workspace, a provider that cannot answer, and no hint that
|
|
2302
2304
|
// `/setup` exists. The launch flag is not evidence of configuration; the
|
|
2303
2305
|
// credential store is, and that is what `stored` reads.
|
|
2304
|
-
if (stored)
|
|
2306
|
+
if (stored) {
|
|
2307
|
+
await this.repairAgentPlaneAtBoot();
|
|
2305
2308
|
return;
|
|
2309
|
+
}
|
|
2306
2310
|
this.pushRow(represent('command-status', { kind: 'system', text: t('onboard.needSetup') }));
|
|
2307
2311
|
await this.runOnboarding();
|
|
2308
2312
|
}
|
|
@@ -2519,10 +2523,29 @@ export class SshTui {
|
|
|
2519
2523
|
const models = Array.isArray(providers[provider]?.models) ? providers[provider]?.models ?? [] : [];
|
|
2520
2524
|
const model = this.agent.options.model ?? this.selectionRef?.current?.model;
|
|
2521
2525
|
const sub = this.subagentSelection.current;
|
|
2526
|
+
// The official DeepSeek adapter is registered through `ctx.llm`, not through
|
|
2527
|
+
// `llm-pi-ai`; the adapter directory is the same source `src/index.ts` uses
|
|
2528
|
+
// for route availability. Keep the configured list too: a pi-ai gateway may
|
|
2529
|
+
// be configured before its adapter has finished registering. Doctor is
|
|
2530
|
+
// best-effort, so an unavailable directory falls back to that list rather
|
|
2531
|
+
// than failing the report.
|
|
2532
|
+
let adapterRoutes = [];
|
|
2533
|
+
const llm = this.ctx.get('llm');
|
|
2534
|
+
if (llm !== undefined) {
|
|
2535
|
+
try {
|
|
2536
|
+
adapterRoutes = llm.listProviders()
|
|
2537
|
+
.map(entry => entry.id.trim())
|
|
2538
|
+
.filter(id => id !== '');
|
|
2539
|
+
}
|
|
2540
|
+
catch {
|
|
2541
|
+
// Keep the configured set; `/doctor` must still render what it knows.
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
const routes = [...new Set([...Object.keys(providers), ...adapterRoutes])];
|
|
2522
2545
|
return {
|
|
2523
2546
|
provider,
|
|
2524
2547
|
...(model === undefined ? {} : { model }),
|
|
2525
|
-
routes
|
|
2548
|
+
routes,
|
|
2526
2549
|
routeModels: models.map(modelEntryId).filter((id) => id !== undefined),
|
|
2527
2550
|
...(sub.provider === undefined ? {} : { subProvider: this.displayProviderId(sub.provider) }),
|
|
2528
2551
|
...(sub.model === undefined ? {} : { subModel: sub.model }),
|
|
@@ -2531,6 +2554,7 @@ export class SshTui {
|
|
|
2531
2554
|
/** One collected `/doctor` snapshot, from this process's own services. */
|
|
2532
2555
|
async collectDoctorFacts(profile) {
|
|
2533
2556
|
const routing = this.doctorRouting();
|
|
2557
|
+
const tools = this.agentPlaneTools();
|
|
2534
2558
|
return collectDoctor({
|
|
2535
2559
|
profile,
|
|
2536
2560
|
dshHome: resolveDshHome(),
|
|
@@ -2538,6 +2562,8 @@ export class SshTui {
|
|
|
2538
2562
|
services: {
|
|
2539
2563
|
roster: this.ctx.get('agentPresets') !== undefined,
|
|
2540
2564
|
codeRuntime: this.hasHostService('codeRuntime'),
|
|
2565
|
+
askUserTool: tools.askUser,
|
|
2566
|
+
presentTool: tools.present,
|
|
2541
2567
|
},
|
|
2542
2568
|
generation: hostSettingsGeneration(this.ctx),
|
|
2543
2569
|
anchors: [process.argv[1], this.ctx.baseUrl],
|
|
@@ -2620,7 +2646,7 @@ export class SshTui {
|
|
|
2620
2646
|
backup = await writePatchWithBackup(facts.patchPath, text);
|
|
2621
2647
|
}
|
|
2622
2648
|
else {
|
|
2623
|
-
await ensureRosterRows(facts.dshHome, facts.profile, rows);
|
|
2649
|
+
await ensureRosterRows(facts.dshHome, facts.profile, rows, facts.generation ?? 'legacy');
|
|
2624
2650
|
}
|
|
2625
2651
|
const done = [
|
|
2626
2652
|
t('doctor.fix.applied', {
|
|
@@ -8391,6 +8417,13 @@ export class SshTui {
|
|
|
8391
8417
|
const options = {
|
|
8392
8418
|
provider,
|
|
8393
8419
|
model,
|
|
8420
|
+
// The reviewer is a direct LLM call outside the Agent loop; the loop normally
|
|
8421
|
+
// supplies this identity. OpenCode requires a per-conversation session
|
|
8422
|
+
// signal: pi-ai's built-in provider turns this into `x-opencode-session`,
|
|
8423
|
+
// while the configured openai-responses route sends `session_id`. Other
|
|
8424
|
+
// providers may use it for their own affinity/cache routing. It must be the
|
|
8425
|
+
// Session that actually asked for approval, never a shared or random id.
|
|
8426
|
+
sessionId: request.agent.session.id,
|
|
8394
8427
|
messages: [createUserMessage({
|
|
8395
8428
|
content: [{ type: 'text', text: buildReviewUserMessage({
|
|
8396
8429
|
userText,
|
|
@@ -10922,18 +10955,91 @@ export class SshTui {
|
|
|
10922
10955
|
this.markDirty();
|
|
10923
10956
|
}
|
|
10924
10957
|
/**
|
|
10925
|
-
*
|
|
10958
|
+
* The two tools a 0.1.7 terminal profile mounts itself, read from the running
|
|
10959
|
+
* composition. The profile patch is only the instruction; this is the answer
|
|
10960
|
+
* to whether the instruction actually produced a tool.
|
|
10961
|
+
*/
|
|
10962
|
+
agentPlaneTools() {
|
|
10963
|
+
const tools = this.ctx.get('tools');
|
|
10964
|
+
return {
|
|
10965
|
+
askUser: tools?.get('ask_user_question') !== undefined,
|
|
10966
|
+
present: tools?.get('present') !== undefined,
|
|
10967
|
+
};
|
|
10968
|
+
}
|
|
10969
|
+
/** Whether this process's agent plane has both tools. */
|
|
10970
|
+
agentPlaneReady() {
|
|
10971
|
+
const tools = this.agentPlaneTools();
|
|
10972
|
+
return tools.askUser && tools.present;
|
|
10973
|
+
}
|
|
10974
|
+
/**
|
|
10975
|
+
* Repair an already-configured profile at boot and say a restart is needed.
|
|
10926
10976
|
*
|
|
10927
|
-
*
|
|
10928
|
-
*
|
|
10929
|
-
*
|
|
10977
|
+
* A first run deliberately does not call this: its wizard save is the point
|
|
10978
|
+
* where settings and credentials exist, and `onboardDone` repairs immediately
|
|
10979
|
+
* after that write. This method serves the old/partial profile upgrade path,
|
|
10980
|
+
* where there is no wizard to wait for.
|
|
10930
10981
|
*/
|
|
10982
|
+
async repairAgentPlaneAtBoot() {
|
|
10983
|
+
const profile = profileFromArgv();
|
|
10984
|
+
try {
|
|
10985
|
+
const repaired = await this.ensureProfileAgentPlane(profile);
|
|
10986
|
+
if (repaired === 'written') {
|
|
10987
|
+
this.pushRow(represent('boot', {
|
|
10988
|
+
kind: 'system',
|
|
10989
|
+
text: t('onboard.agentPlaneAdded', {
|
|
10990
|
+
path: rosterPatchPath(resolveDshHome(), profile),
|
|
10991
|
+
profile,
|
|
10992
|
+
}),
|
|
10993
|
+
}));
|
|
10994
|
+
this.refreshRosterHealth();
|
|
10995
|
+
this.markDirty();
|
|
10996
|
+
}
|
|
10997
|
+
}
|
|
10998
|
+
catch (error) {
|
|
10999
|
+
// A broken or unwritable patch must not be swallowed: the workspace can
|
|
11000
|
+
// still boot, but the missing tools stay visible in the footer and
|
|
11001
|
+
// `/doctor` explains how to repair the file.
|
|
11002
|
+
this.pushRow(represent('boot', {
|
|
11003
|
+
kind: 'error',
|
|
11004
|
+
text: t('onboard.agentPlaneFailed', {
|
|
11005
|
+
error: errorChain(error),
|
|
11006
|
+
path: rosterPatchPath(resolveDshHome(), profile),
|
|
11007
|
+
}),
|
|
11008
|
+
}));
|
|
11009
|
+
this.markDirty();
|
|
11010
|
+
}
|
|
11011
|
+
}
|
|
11012
|
+
/**
|
|
11013
|
+
* Add only the profile rows whose tools are missing from this composition.
|
|
11014
|
+
*
|
|
11015
|
+
* A row that is already present in the patch is never written again, even if
|
|
11016
|
+
* its service failed to activate: the planner sees overrides and disables by
|
|
11017
|
+
* id or module name, and `ensureRosterRows` creates a backup first. This is
|
|
11018
|
+
* the first-run/upgrade repair, not an install-hook claim: it changes what
|
|
11019
|
+
* the next boot composes, not what this process has already composed.
|
|
11020
|
+
*/
|
|
11021
|
+
async ensureProfileAgentPlane(profile = profileFromArgv()) {
|
|
11022
|
+
const generation = this.settingsGeneration;
|
|
11023
|
+
const tools = this.agentPlaneTools();
|
|
11024
|
+
const missing = generation === 'forms'
|
|
11025
|
+
? rosterRows(generation).filter(row => row.id === 'tool-ask-user' ? !tools.askUser : !tools.present)
|
|
11026
|
+
: rosterRows(generation).filter(row => row.id === 'agent-presets'
|
|
11027
|
+
? this.ctx.get('agentPresets') === undefined
|
|
11028
|
+
: row.id === 'code-runtime'
|
|
11029
|
+
? !this.hasHostService('codeRuntime')
|
|
11030
|
+
: true);
|
|
11031
|
+
if (missing.length === 0)
|
|
11032
|
+
return 'present';
|
|
11033
|
+
return ensureRosterRows(resolveDshHome(), profile, missing, generation);
|
|
11034
|
+
}
|
|
10931
11035
|
/** Re-read the roster fact this process was composed with (cheap, no IO). */
|
|
10932
11036
|
refreshRosterHealth() {
|
|
10933
11037
|
// 0.1.7 has no roster to be missing: what a terminal profile owns there is
|
|
10934
|
-
// the agent-plane
|
|
11038
|
+
// the agent-plane tools, so the chip follows the tools the composition
|
|
11039
|
+
// actually registered. Reading the patch alone would call a broken load
|
|
11040
|
+
// healthy as soon as the row text was written.
|
|
10935
11041
|
this.rosterMissing = this.settingsGeneration === 'forms'
|
|
10936
|
-
? !
|
|
11042
|
+
? !this.agentPlaneReady()
|
|
10937
11043
|
: this.ctx.get('agentPresets') === undefined;
|
|
10938
11044
|
}
|
|
10939
11045
|
/** Which settings protocol this host speaks; decides what "missing" means. */
|
|
@@ -10954,6 +11060,14 @@ export class SshTui {
|
|
|
10954
11060
|
? [first, t('mode.formsHost'), t('mode.formsHint', { profile, patch })].join('\n')
|
|
10955
11061
|
: [first, t('mode.missingServiceHint', { profile, patch })].join('\n');
|
|
10956
11062
|
}
|
|
11063
|
+
/**
|
|
11064
|
+
* `/mode fix`: write the missing profile rows into the user patch layer.
|
|
11065
|
+
*
|
|
11066
|
+
* The write only takes effect on the next launch — the loader composes the
|
|
11067
|
+
* patch tree once at boot — so the report says so instead of pretending the
|
|
11068
|
+
* running session gained a roster. The helper consults the actual tools, so
|
|
11069
|
+
* a row a bundle already supplies is not mounted a second time.
|
|
11070
|
+
*/
|
|
10957
11071
|
async repairRoster() {
|
|
10958
11072
|
const profile = profileFromArgv();
|
|
10959
11073
|
const patch = rosterPatchPath(resolveDshHome(), profile);
|
|
@@ -10962,8 +11076,7 @@ export class SshTui {
|
|
|
10962
11076
|
// process-wide and owns the agent-plane rows instead of the roster. Both
|
|
10963
11077
|
// the list and the header come from the same generation `/doctor` uses,
|
|
10964
11078
|
// or this writes rows the running host cannot resolve.
|
|
10965
|
-
const
|
|
10966
|
-
const result = await ensureRosterRows(resolveDshHome(), profile, rosterRows(generation), generation);
|
|
11079
|
+
const result = await this.ensureProfileAgentPlane(profile);
|
|
10967
11080
|
this.refreshRosterHealth();
|
|
10968
11081
|
this.pushRow(represent('repair-result', {
|
|
10969
11082
|
kind: 'system',
|
|
@@ -13032,7 +13145,7 @@ export class SshTui {
|
|
|
13032
13145
|
this.setupMessage(state, 'system', t('onboard.baseSaved', { path: displayDshPath('settings.yaml') }));
|
|
13033
13146
|
}
|
|
13034
13147
|
if (saved) {
|
|
13035
|
-
this.onboardDone(t('onboard.officialDone', { model }));
|
|
13148
|
+
await this.onboardDone(t('onboard.officialDone', { model }));
|
|
13036
13149
|
}
|
|
13037
13150
|
}
|
|
13038
13151
|
else {
|
|
@@ -13205,7 +13318,7 @@ export class SshTui {
|
|
|
13205
13318
|
this.onSelectionChanged?.(selection);
|
|
13206
13319
|
await this.rememberRoute(selection);
|
|
13207
13320
|
await this.syncSubagentToProvider(ownerId, state.models, previousParentProvider !== ownerId);
|
|
13208
|
-
this.onboardDone(t('onboard.customDone', { id: ownerId, model }));
|
|
13321
|
+
await this.onboardDone(t('onboard.customDone', { id: ownerId, model }));
|
|
13209
13322
|
}
|
|
13210
13323
|
}
|
|
13211
13324
|
}
|
|
@@ -13249,8 +13362,40 @@ export class SshTui {
|
|
|
13249
13362
|
* answer: `command-status` routes to the transcript, exactly like the boot line
|
|
13250
13363
|
* that asked for the setup in the first place.
|
|
13251
13364
|
*/
|
|
13252
|
-
onboardDone(text) {
|
|
13365
|
+
async onboardDone(text) {
|
|
13366
|
+
const profile = profileFromArgv();
|
|
13367
|
+
let repairNote;
|
|
13368
|
+
try {
|
|
13369
|
+
// The wizard wrote settings and the model selection through the profile
|
|
13370
|
+
// patch. Only after those writes are complete does the agent-plane repair
|
|
13371
|
+
// run, so it cannot be undone by the wizard's own patch update.
|
|
13372
|
+
const repaired = await this.ensureProfileAgentPlane(profile);
|
|
13373
|
+
if (repaired === 'written') {
|
|
13374
|
+
repairNote = t('onboard.agentPlaneAdded', {
|
|
13375
|
+
path: rosterPatchPath(resolveDshHome(), profile),
|
|
13376
|
+
profile,
|
|
13377
|
+
});
|
|
13378
|
+
}
|
|
13379
|
+
else if (this.settingsGeneration === 'forms' && !this.agentPlaneReady()) {
|
|
13380
|
+
// The rows are there but this process was composed before them (or a
|
|
13381
|
+
// row failed to activate). Saying "saved" alone would hide that the
|
|
13382
|
+
// tools are still absent from this session.
|
|
13383
|
+
repairNote = t('onboard.agentPlaneRestart', {
|
|
13384
|
+
path: rosterPatchPath(resolveDshHome(), profile),
|
|
13385
|
+
profile,
|
|
13386
|
+
});
|
|
13387
|
+
}
|
|
13388
|
+
}
|
|
13389
|
+
catch (error) {
|
|
13390
|
+
repairNote = t('onboard.agentPlaneFailed', {
|
|
13391
|
+
error: errorChain(error),
|
|
13392
|
+
path: rosterPatchPath(resolveDshHome(), profile),
|
|
13393
|
+
});
|
|
13394
|
+
}
|
|
13253
13395
|
this.pushRow(represent('command-status', { kind: 'system', text }));
|
|
13396
|
+
if (repairNote !== undefined) {
|
|
13397
|
+
this.pushRow(represent('runtime-warning', { kind: 'system', text: repairNote }));
|
|
13398
|
+
}
|
|
13254
13399
|
this.markDirty();
|
|
13255
13400
|
}
|
|
13256
13401
|
/** Store one credential, falling back to a launch-environment override on shadow/absence. */
|