@deepseek-harness-tui/dsh-tui 0.7.1 → 0.7.3
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 +7 -5
- package/bin/dsh-tui.js +42 -1
- package/cordis.patch.yml +10 -0
- package/lib/types/commands.js +1 -1
- package/lib/types/components/GoalTodoPanel.d.ts.map +1 -1
- package/lib/types/components/GoalTodoPanel.js +6 -1
- package/lib/types/components/HistorySearchDialog.d.ts.map +1 -1
- package/lib/types/components/HistorySearchDialog.js +5 -1
- package/lib/types/components/LoadedContextPanel.d.ts.map +1 -1
- package/lib/types/components/LoadedContextPanel.js +1 -1
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +5 -1
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +52 -74
- package/lib/types/components/SearchBox.d.ts +4 -0
- package/lib/types/components/SearchBox.d.ts.map +1 -1
- package/lib/types/components/SearchBox.js +101 -2
- package/lib/types/components/Spinner/SpinnerAnimationRow.d.ts +4 -1
- package/lib/types/components/Spinner/SpinnerAnimationRow.d.ts.map +1 -1
- package/lib/types/components/Spinner/SpinnerAnimationRow.js +10 -5
- package/lib/types/components/WorkingSpinner.d.ts +3 -1
- package/lib/types/components/WorkingSpinner.d.ts.map +1 -1
- package/lib/types/components/WorkingSpinner.js +2 -2
- package/lib/types/components/design-system/ThemedText.d.ts +9 -3
- package/lib/types/components/design-system/ThemedText.d.ts.map +1 -1
- package/lib/types/components/design-system/ThemedText.js +3 -5
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.js +52 -8
- package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
- package/lib/types/components/questions/PlanReviewPanel.js +12 -3
- package/lib/types/components/sessions/SessionListRow.d.ts +26 -0
- package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -0
- package/lib/types/components/sessions/SessionListRow.js +35 -0
- package/lib/types/components/sessions/SessionPreview.d.ts +25 -0
- package/lib/types/components/sessions/SessionPreview.d.ts.map +1 -0
- package/lib/types/components/sessions/SessionPreview.js +60 -0
- package/lib/types/dsh-adapter/channel.d.ts +19 -4
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +76 -82
- package/lib/types/dsh-adapter/compat/index.d.ts +1 -1
- package/lib/types/dsh-adapter/compat/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/index.js +1 -1
- package/lib/types/dsh-adapter/compat/sessionLog.d.ts +8 -0
- package/lib/types/dsh-adapter/compat/sessionLog.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/sessionLog.js +1 -1
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +10 -1
- package/lib/types/dsh-adapter/packaged-presets.d.ts +22 -0
- package/lib/types/dsh-adapter/packaged-presets.d.ts.map +1 -0
- package/lib/types/dsh-adapter/packaged-presets.js +93 -0
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +53 -7
- package/lib/types/dsh-adapter/presets.d.ts +16 -0
- package/lib/types/dsh-adapter/presets.d.ts.map +1 -1
- package/lib/types/dsh-adapter/presets.js +23 -0
- package/lib/types/dsh-adapter/providerWizard.d.ts.map +1 -1
- package/lib/types/dsh-adapter/providerWizard.js +3 -1
- package/lib/types/dsh-adapter/sessions/digest.d.ts +29 -0
- package/lib/types/dsh-adapter/sessions/digest.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sessions/digest.js +224 -0
- package/lib/types/dsh-adapter/sessions/frames.d.ts +100 -0
- package/lib/types/dsh-adapter/sessions/frames.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sessions/frames.js +246 -0
- package/lib/types/dsh-adapter/sessions/header.d.ts +50 -0
- package/lib/types/dsh-adapter/sessions/header.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sessions/header.js +62 -0
- package/lib/types/dsh-adapter/sessions/index.d.ts +17 -0
- package/lib/types/dsh-adapter/sessions/index.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sessions/index.js +15 -0
- package/lib/types/dsh-adapter/sessions/list.d.ts +41 -0
- package/lib/types/dsh-adapter/sessions/list.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sessions/list.js +213 -0
- package/lib/types/dsh-adapter/sessions/store.d.ts +48 -0
- package/lib/types/dsh-adapter/sessions/store.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sessions/store.js +164 -0
- package/lib/types/dsh-adapter/sessions/types.d.ts +142 -0
- package/lib/types/dsh-adapter/sessions/types.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sessions/types.js +14 -0
- package/lib/types/i18n.d.ts +150 -22
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +45 -9
- package/lib/types/ink/components/Text.d.ts +9 -1
- package/lib/types/ink/components/Text.d.ts.map +1 -1
- package/lib/types/ink/components/Text.js +7 -6
- package/lib/types/ink/ink.d.ts.map +1 -1
- package/lib/types/ink/ink.js +4 -2
- package/lib/types/ink/log-update.d.ts.map +1 -1
- package/lib/types/ink/log-update.js +76 -9
- package/lib/types/ink/parse-keypress.d.ts.map +1 -1
- package/lib/types/ink/parse-keypress.js +4 -1
- package/lib/types/ink/terminal.d.ts +40 -4
- package/lib/types/ink/terminal.d.ts.map +1 -1
- package/lib/types/ink/terminal.js +57 -5
- package/lib/types/ink/termio/tokenize.d.ts +6 -0
- package/lib/types/ink/termio/tokenize.d.ts.map +1 -1
- package/lib/types/ink/termio/tokenize.js +18 -4
- package/lib/types/screens/Chat.d.ts +3 -2
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +104 -160
- package/lib/types/screens/SessionBrowser.d.ts +37 -0
- package/lib/types/screens/SessionBrowser.d.ts.map +1 -0
- package/lib/types/screens/SessionBrowser.js +452 -0
- package/lib/types/sessionHistory.d.ts +0 -8
- package/lib/types/sessionHistory.d.ts.map +1 -1
- package/lib/types/sessions/format.d.ts +117 -0
- package/lib/types/sessions/format.d.ts.map +1 -0
- package/lib/types/sessions/format.js +244 -0
- package/lib/types/sessions/view.d.ts +125 -0
- package/lib/types/sessions/view.d.ts.map +1 -0
- package/lib/types/sessions/view.js +234 -0
- package/lib/types/update.d.ts +21 -7
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +66 -16
- package/lib/types/utils/paths.d.ts.map +1 -1
- package/lib/types/utils/paths.js +4 -1
- package/package.json +37 -9
- package/presets/liangshen/.dsh-tui-managed.json +5 -0
- package/presets/liangshen/agent.cordis.yml +389 -0
- package/presets/liangshen/compaction-epoch.mjs +81 -0
- package/presets/liangshen/custom-bash.mjs +126 -0
- package/presets/liangshen/instruction-hint.mjs +181 -0
- package/presets/liangshen/preset.yml +3 -0
- package/presets/liangshen/skill-search.mjs +142 -0
- package/presets/liangshen/tool-bootstrap.mjs +300 -0
- package/lib/types/components/ResumePicker.d.ts +0 -26
- package/lib/types/components/ResumePicker.d.ts.map +0 -1
- package/lib/types/components/ResumePicker.js +0 -50
|
@@ -5,6 +5,8 @@ import { AlternateScreen, Box, Text, useInput, ScrollBox, useTheme, useTerminalS
|
|
|
5
5
|
import { POINTER } from '../cc/figures.js';
|
|
6
6
|
import { isMod, isPlainReturnInput, modLabel } from '../utils/modifiers.js';
|
|
7
7
|
import { formatTokens } from '../cc/format.js';
|
|
8
|
+
import { homeDir } from '../utils/paths.js';
|
|
9
|
+
import { sessionCwdMatches } from '../dsh-adapter/channel.js';
|
|
8
10
|
import { runProviderWizard } from '../dsh-adapter/providerWizard.js';
|
|
9
11
|
import { ApprovalStore } from '../dsh-adapter/approvals.js';
|
|
10
12
|
import { AskUserQuestionPanel } from '../components/questions/AskUserQuestionPanel.js';
|
|
@@ -24,7 +26,7 @@ import { StatusLine } from './StatusLine.js';
|
|
|
24
26
|
import { WorkingSpinner, useThinkingStatus } from '../components/WorkingSpinner.js';
|
|
25
27
|
import { ActivityLine, contextPressurePct } from '../components/ActivityLine.js';
|
|
26
28
|
import { ModelPicker } from '../components/ModelPicker.js';
|
|
27
|
-
import {
|
|
29
|
+
import { SessionBrowser } from './SessionBrowser.js';
|
|
28
30
|
import { WorkspacePicker } from '../components/WorkspacePicker.js';
|
|
29
31
|
import { WorkspaceFlowPicker } from '../components/WorkspaceFlowPicker.js';
|
|
30
32
|
import { ActivityPicker } from '../components/ActivityPicker.js';
|
|
@@ -150,12 +152,9 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
150
152
|
const [modelPickerOpen, setModelPickerOpen] = React.useState(false);
|
|
151
153
|
const [models, setModels] = React.useState([]);
|
|
152
154
|
const [modelIndex, setModelIndex] = React.useState(0);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const [
|
|
156
|
-
/** `/resume` session management (issue #112): plain selection, a delete
|
|
157
|
-
* confirmation (ctrl+d), or the inline rename input (ctrl+r). */
|
|
158
|
-
const [resumeMode, setResumeMode] = React.useState('list');
|
|
155
|
+
/** `/resume` opens the session browser, a screen rather than a panel. It
|
|
156
|
+
* owns its own selection, filters and keyboard — Chat only opens it. */
|
|
157
|
+
const [browserOpen, setBrowserOpen] = React.useState(false);
|
|
159
158
|
const [workspacePickerOpen, setWorkspacePickerOpen] = React.useState(false);
|
|
160
159
|
const [workspaceTargets, setWorkspaceTargets] = React.useState([]);
|
|
161
160
|
const [workspaceIndex, setWorkspaceIndex] = React.useState(0);
|
|
@@ -165,7 +164,6 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
165
164
|
const [workspaceFlowInput, setWorkspaceFlowInput] = React.useState(null);
|
|
166
165
|
const workspaceFlowRequestRef = React.useRef(0);
|
|
167
166
|
const workspaceFlowAbortRef = React.useRef(null);
|
|
168
|
-
const [resumeRenameText, setResumeRenameText] = React.useState('');
|
|
169
167
|
/** `/activity` indicator picker (pi extension's interactive select). */
|
|
170
168
|
const [activityPickerOpen, setActivityPickerOpen] = React.useState(false);
|
|
171
169
|
const [activityIndex, setActivityIndex] = React.useState(0);
|
|
@@ -245,6 +243,14 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
245
243
|
}, []);
|
|
246
244
|
/** Startup context panel: expanded by header click or Ctrl+T. */
|
|
247
245
|
const [loadedContextOpen, setLoadedContextOpen] = React.useState(false);
|
|
246
|
+
/**
|
|
247
|
+
* Whether the startup context panel is on screen. Derived once and read by
|
|
248
|
+
* BOTH the Ctrl+T handler and the render below: the key's meaning depends on
|
|
249
|
+
* the panel being visible, so a second copy of this condition is a place for
|
|
250
|
+
* the two to disagree — which is how the key came to advertise one thing and
|
|
251
|
+
* do another in the first place.
|
|
252
|
+
*/
|
|
253
|
+
const loadedContextVisible = channel.rows.length === 0 && channel.loadedContext !== undefined;
|
|
248
254
|
/** `/` transcript search (less-style incsearch, ported from CC's REPL). */
|
|
249
255
|
const [searchOpen, setSearchOpen] = React.useState(false);
|
|
250
256
|
const [searchQuery, setSearchQuery] = React.useState('');
|
|
@@ -308,10 +314,17 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
308
314
|
// Spinner timing refs, fed from channel state each render (the spinner
|
|
309
315
|
// only mounts while working, so values are stable for the mount).
|
|
310
316
|
const responseLengthRef = React.useRef(0);
|
|
317
|
+
const uploadTokensRef = React.useRef(0);
|
|
311
318
|
const loadingStartTimeRef = React.useRef(0);
|
|
312
319
|
const totalPausedMsRef = React.useRef(0);
|
|
313
320
|
const pauseStartTimeRef = React.useRef(null);
|
|
314
321
|
responseLengthRef.current = channel.responseChars;
|
|
322
|
+
// Most recent request's real upload (input + cache read/write occupy the
|
|
323
|
+
// wire exactly like the context window); 0 until the first usage event.
|
|
324
|
+
const lastUploadTokens = channel.lastUsage === undefined
|
|
325
|
+
? 0
|
|
326
|
+
: channel.lastUsage.input + channel.lastUsage.cacheRead + channel.lastUsage.cacheWrite;
|
|
327
|
+
uploadTokensRef.current = lastUploadTokens;
|
|
315
328
|
loadingStartTimeRef.current = channel.turnStart;
|
|
316
329
|
const thinkingStatus = useThinkingStatus(channel.spinnerMode === 'thinking');
|
|
317
330
|
// Terminal tab title (ported from CC's AnimatedTerminalTitle): the session
|
|
@@ -663,22 +676,10 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
663
676
|
}
|
|
664
677
|
case 'resume': {
|
|
665
678
|
setHelpOpen(false);
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
// otherwise the fresh empty session of this launch always tops the
|
|
671
|
-
// list as an unopenable row.
|
|
672
|
-
const pickable = sessions.filter(session => session.id !== channel.agentId);
|
|
673
|
-
setResumeSessions(pickable);
|
|
674
|
-
if (pickable.length === 0) {
|
|
675
|
-
channel.notify(t('resume-none-in-cwd'));
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
setResumeMode('list');
|
|
679
|
-
setResumePickerOpen(true);
|
|
680
|
-
setResumeIndex(0);
|
|
681
|
-
})();
|
|
679
|
+
// The browser opens immediately and loads its own list. Waiting for
|
|
680
|
+
// the listing here would make `/resume` feel slower the more history
|
|
681
|
+
// a project has, which is exactly backwards.
|
|
682
|
+
setBrowserOpen(true);
|
|
682
683
|
return true;
|
|
683
684
|
}
|
|
684
685
|
case 'workspace': {
|
|
@@ -828,14 +829,28 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
828
829
|
});
|
|
829
830
|
return true;
|
|
830
831
|
case 'login': {
|
|
831
|
-
const key = process.env.DEEPSEEK_API_KEY;
|
|
832
|
-
const lines = [
|
|
833
|
-
`${t('login-api-key', { key: key ? key.slice(0, 6) + '…' + key.slice(-4) : t('login-key-missing') })}`,
|
|
834
|
-
`${t('login-base-url', { url: process.env.DEEPSEEK_BASE_URL ?? t('login-official-endpoint') })}`,
|
|
835
|
-
t('login-source-hint'),
|
|
836
|
-
];
|
|
837
832
|
setHelpOpen(false);
|
|
838
|
-
channel.
|
|
833
|
+
void channel.describeCredential('DEEPSEEK_API_KEY')
|
|
834
|
+
.catch(() => undefined)
|
|
835
|
+
.then(status => {
|
|
836
|
+
const keyStatus = status === undefined
|
|
837
|
+
? t('login-credentials-unavailable')
|
|
838
|
+
: status.configured
|
|
839
|
+
? t('login-key-configured', { ref: 'DEEPSEEK_API_KEY' })
|
|
840
|
+
: t('login-key-missing');
|
|
841
|
+
channel.pushLocal('/login', [
|
|
842
|
+
t('login-api-key', { status: keyStatus }),
|
|
843
|
+
...(status === undefined
|
|
844
|
+
? []
|
|
845
|
+
: [
|
|
846
|
+
t('login-credential-source', { source: status.source ?? t('login-source-none') }),
|
|
847
|
+
t('login-credential-storage', {
|
|
848
|
+
mode: t(status.writable ? 'login-storage-writable' : 'login-storage-read-only'),
|
|
849
|
+
}),
|
|
850
|
+
]),
|
|
851
|
+
t('login-base-url', { url: process.env.DEEPSEEK_BASE_URL ?? t('login-official-endpoint') }),
|
|
852
|
+
]);
|
|
853
|
+
});
|
|
839
854
|
return true;
|
|
840
855
|
}
|
|
841
856
|
case 'logout':
|
|
@@ -1182,6 +1197,11 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1182
1197
|
// not block the event from reaching the panel.
|
|
1183
1198
|
if (btw !== null)
|
|
1184
1199
|
return;
|
|
1200
|
+
// Same for the session browser: it renders instead of the conversation,
|
|
1201
|
+
// so every key belongs to it — including the plain letters that drive its
|
|
1202
|
+
// search box, which Chat would otherwise route into the prompt.
|
|
1203
|
+
if (browserOpen)
|
|
1204
|
+
return;
|
|
1185
1205
|
// The questionnaire / approval panel owns the keyboard while one is
|
|
1186
1206
|
// pending (the panel's own useInput handles ↑/↓/Space/Tab/Enter/Esc;
|
|
1187
1207
|
// the prompt input is unmounted, so nothing else should see these keys).
|
|
@@ -1407,123 +1427,6 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1407
1427
|
}
|
|
1408
1428
|
return;
|
|
1409
1429
|
}
|
|
1410
|
-
if (resumePickerOpen) {
|
|
1411
|
-
const resumeSession = resumeSessions[resumeIndex];
|
|
1412
|
-
// Session management modes (issue #112): the list keys stay untouched
|
|
1413
|
-
// until ctrl+d/ctrl+r switch into a sub-mode, each with Enter/Esc.
|
|
1414
|
-
if (resumeMode === 'confirm-delete') {
|
|
1415
|
-
if (plainReturn) {
|
|
1416
|
-
setResumeMode('list');
|
|
1417
|
-
// oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime guard: out-of-range index on an empty list
|
|
1418
|
-
if (resumeSession) {
|
|
1419
|
-
const target = resumeSession;
|
|
1420
|
-
void (async () => {
|
|
1421
|
-
const ok = await channel.deleteSession(target.id);
|
|
1422
|
-
if (!ok) {
|
|
1423
|
-
channel.notify(t('resume-delete-failed', { name: target.title || target.id }), { color: 'error' });
|
|
1424
|
-
return;
|
|
1425
|
-
}
|
|
1426
|
-
channel.notify(t('resume-deleted', { name: target.title || target.id }));
|
|
1427
|
-
// Refresh right away so the row disappears in place; closing
|
|
1428
|
-
// the picker when nothing resumable remains.
|
|
1429
|
-
const sessions = await channel.listSessions();
|
|
1430
|
-
const pickable = sessions.filter(session => session.id !== channel.agentId);
|
|
1431
|
-
setResumeSessions(pickable);
|
|
1432
|
-
if (pickable.length === 0) {
|
|
1433
|
-
setResumePickerOpen(false);
|
|
1434
|
-
}
|
|
1435
|
-
else {
|
|
1436
|
-
setResumeIndex(index => Math.min(index, pickable.length - 1));
|
|
1437
|
-
}
|
|
1438
|
-
})();
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
else if (key.escape) {
|
|
1442
|
-
setResumeMode('list');
|
|
1443
|
-
}
|
|
1444
|
-
return;
|
|
1445
|
-
}
|
|
1446
|
-
if (resumeMode === 'rename') {
|
|
1447
|
-
if (plainReturn) {
|
|
1448
|
-
setResumeMode('list');
|
|
1449
|
-
const title = resumeRenameText.trim();
|
|
1450
|
-
// oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime guard: out-of-range index on an empty list
|
|
1451
|
-
if (resumeSession && title.length > 0) {
|
|
1452
|
-
const target = resumeSession;
|
|
1453
|
-
void (async () => {
|
|
1454
|
-
const ok = await channel.renameSessionTo(target.id, title);
|
|
1455
|
-
if (!ok) {
|
|
1456
|
-
channel.notify(t('resume-rename-failed', { name: target.title || target.id }), { color: 'error' });
|
|
1457
|
-
return;
|
|
1458
|
-
}
|
|
1459
|
-
channel.notify(t('rename-done', { title }));
|
|
1460
|
-
// Re-list so the row reflects the persisted state, but patch
|
|
1461
|
-
// the renamed row's title explicitly: listSessions resolves
|
|
1462
|
-
// persisted titles only within the MRU top SESSION_TITLE_DEPTH
|
|
1463
|
-
// window, and a freshly renamed row must never snap back to
|
|
1464
|
-
// the basename fallback in between.
|
|
1465
|
-
const sessions = await channel.listSessions();
|
|
1466
|
-
const next = sessions
|
|
1467
|
-
.filter(session => session.id !== channel.agentId)
|
|
1468
|
-
.map(session => (session.id === target.id ? { ...session, title } : session));
|
|
1469
|
-
setResumeSessions(next);
|
|
1470
|
-
// Re-anchor focus on the renamed row: renameSessionTo touches
|
|
1471
|
-
// MRU, so the re-listed order shifts — a kept index would
|
|
1472
|
-
// silently point at a DIFFERENT session, and a following
|
|
1473
|
-
// Enter/ctrl+d would act on the wrong one (review leftover).
|
|
1474
|
-
const anchored = next.findIndex(session => session.id === target.id);
|
|
1475
|
-
if (anchored >= 0)
|
|
1476
|
-
setResumeIndex(anchored);
|
|
1477
|
-
})();
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
else if (key.escape) {
|
|
1481
|
-
setResumeMode('list');
|
|
1482
|
-
}
|
|
1483
|
-
else if (key.backspace) {
|
|
1484
|
-
setResumeRenameText(text => text.slice(0, -1));
|
|
1485
|
-
}
|
|
1486
|
-
else if (!key.ctrl && !key.meta && !key.super && input) {
|
|
1487
|
-
// Single-line title: pasted newlines collapse to spaces.
|
|
1488
|
-
setResumeRenameText(text => text + input.replace(/[\r\n]+/g, ' '));
|
|
1489
|
-
}
|
|
1490
|
-
return;
|
|
1491
|
-
}
|
|
1492
|
-
if (key.upArrow) {
|
|
1493
|
-
setResumeIndex(index => (index <= 0 ? resumeSessions.length - 1 : index - 1));
|
|
1494
|
-
}
|
|
1495
|
-
else if (key.downArrow) {
|
|
1496
|
-
setResumeIndex(index => (index >= resumeSessions.length - 1 ? 0 : index + 1));
|
|
1497
|
-
}
|
|
1498
|
-
else if (plainReturn) {
|
|
1499
|
-
// oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime guard: out-of-range index on an empty list
|
|
1500
|
-
if (resumeSession) {
|
|
1501
|
-
// Enter switches the live agent to the persisted session right
|
|
1502
|
-
// away (the history replays into the transcript); the resume.txt
|
|
1503
|
-
// launcher marker is refreshed by resumeTo so `--resume` on the
|
|
1504
|
-
// next launch opens the same session.
|
|
1505
|
-
setResumePickerOpen(false);
|
|
1506
|
-
void channel.resumeTo(resumeSession.id).then((ok) => {
|
|
1507
|
-
if (ok)
|
|
1508
|
-
channel.notify(t('resume-resumed'));
|
|
1509
|
-
});
|
|
1510
|
-
}
|
|
1511
|
-
else {
|
|
1512
|
-
setResumePickerOpen(false);
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
else if (key.escape) {
|
|
1516
|
-
setResumePickerOpen(false);
|
|
1517
|
-
}
|
|
1518
|
-
else if (isMod(key) && input === 'd' && resumeSession) {
|
|
1519
|
-
setResumeMode('confirm-delete');
|
|
1520
|
-
}
|
|
1521
|
-
else if (isMod(key) && input === 'r' && resumeSession) {
|
|
1522
|
-
setResumeRenameText(resumeSession.title || '');
|
|
1523
|
-
setResumeMode('rename');
|
|
1524
|
-
}
|
|
1525
|
-
return;
|
|
1526
|
-
}
|
|
1527
1430
|
if (modelPickerOpen) {
|
|
1528
1431
|
if (key.upArrow) {
|
|
1529
1432
|
setModelIndex(index => (index <= 0 ? models.length - 1 : index - 1));
|
|
@@ -1664,10 +1567,22 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1664
1567
|
}
|
|
1665
1568
|
}
|
|
1666
1569
|
else if (key.leftArrow) {
|
|
1667
|
-
|
|
1570
|
+
// Step by code point, not UTF-16 unit: an emoji is two units, and
|
|
1571
|
+
// a mid-pair caret offset would split it in the SearchBox render.
|
|
1572
|
+
setHistoryCursor(cursor => {
|
|
1573
|
+
if (cursor <= 0)
|
|
1574
|
+
return 0;
|
|
1575
|
+
const ch = [...historyQuery.slice(0, cursor)].pop();
|
|
1576
|
+
return cursor - ch.length;
|
|
1577
|
+
});
|
|
1668
1578
|
}
|
|
1669
1579
|
else if (key.rightArrow) {
|
|
1670
|
-
setHistoryCursor(cursor =>
|
|
1580
|
+
setHistoryCursor(cursor => {
|
|
1581
|
+
if (cursor >= historyQuery.length)
|
|
1582
|
+
return historyQuery.length;
|
|
1583
|
+
const ch = [...historyQuery.slice(cursor)][0];
|
|
1584
|
+
return cursor + ch.length;
|
|
1585
|
+
});
|
|
1671
1586
|
}
|
|
1672
1587
|
else if (key.home) {
|
|
1673
1588
|
setHistoryCursor(0);
|
|
@@ -1715,11 +1630,25 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1715
1630
|
return;
|
|
1716
1631
|
}
|
|
1717
1632
|
if (isMod(key) && input === 't') {
|
|
1718
|
-
// Ctrl+T
|
|
1719
|
-
//
|
|
1720
|
-
//
|
|
1721
|
-
//
|
|
1722
|
-
//
|
|
1633
|
+
// Ctrl+T means "expand what is in front of you", and the two things it
|
|
1634
|
+
// can expand never share the screen.
|
|
1635
|
+
//
|
|
1636
|
+
// The startup loaded-context panel renders only while the transcript is
|
|
1637
|
+
// empty (see the render below) — and that is exactly the window where
|
|
1638
|
+
// the trajectory has nothing in it yet, since the trajectory is folded
|
|
1639
|
+
// from the session's own events. So the panel wins while it is up, and
|
|
1640
|
+
// the scene takes over for the rest of the session.
|
|
1641
|
+
//
|
|
1642
|
+
// Claiming the key outright for the scene was wrong even though the
|
|
1643
|
+
// panel binding looked dead: the panel prints its own `(Ctrl+T 展开)`
|
|
1644
|
+
// hint, so the key still had a visible promise attached to it, and the
|
|
1645
|
+
// scene it opened instead was necessarily empty at that moment. That
|
|
1646
|
+
// left the panel reachable only by clicking its header, which is not a
|
|
1647
|
+
// keyboard path at all.
|
|
1648
|
+
if (loadedContextVisible) {
|
|
1649
|
+
setLoadedContextOpen(previous => !previous);
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1723
1652
|
openScene();
|
|
1724
1653
|
return;
|
|
1725
1654
|
}
|
|
@@ -1815,8 +1744,18 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1815
1744
|
// Working-activity line (spinner slot): context-pressure prefix shares the
|
|
1816
1745
|
// StatusLine thresholds (amber ≥ 80, red ≥ 95).
|
|
1817
1746
|
const activityWarnPct = contextPressurePct(channel.lastUsage, channel.contextWindow);
|
|
1747
|
+
// The browser is a screen, not an overlay: it REPLACES the conversation
|
|
1748
|
+
// rather than floating above it. Rendering it as an early return (after
|
|
1749
|
+
// every hook above has run) is what makes that literal — there is no
|
|
1750
|
+
// transcript underneath to be repainted, scrolled, or bled through.
|
|
1751
|
+
if (browserOpen) {
|
|
1752
|
+
const browser = (_jsx(SessionBrowser, { channel: channel, home: homeDir(), sameProject: sessionCwdMatches, onClose: () => setBrowserOpen(false) }));
|
|
1753
|
+
// Inline hosts enter the alternate screen for the duration; full-screen
|
|
1754
|
+
// hosts are already in it and must not nest a second one.
|
|
1755
|
+
return fullscreen ? browser : _jsx(AlternateScreen, { children: browser });
|
|
1756
|
+
}
|
|
1818
1757
|
/** Prompt input is inert while a modal dialog owns the keyboard. */
|
|
1819
|
-
const promptSelectionActive = selectionActive || modelPickerOpen ||
|
|
1758
|
+
const promptSelectionActive = selectionActive || modelPickerOpen || workspacePickerOpen || workspaceFlow !== null || activityPickerOpen ||
|
|
1820
1759
|
effortSliderOpen || presetPickerOpen || themePickerOpen || thinkingOpen || historyOpen || rewindOpen || searchOpen ||
|
|
1821
1760
|
btw !== null;
|
|
1822
1761
|
// The trajectory scene replaces the conversation for as long as it is open.
|
|
@@ -1837,7 +1776,7 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1837
1776
|
// 常驻、只移除其普通子节点,blit 解毒不触发,被覆盖的转录行会在
|
|
1838
1777
|
// blit-skip 后留空(Esc 关 picker 一片空白的根因)。
|
|
1839
1778
|
const dialogOverlayOpen = thinkingOpen || (workspacePickerOpen && workspaceTargets.length > 0) || workspaceFlow !== null ||
|
|
1840
|
-
|
|
1779
|
+
modelPickerOpen ||
|
|
1841
1780
|
activityPickerOpen || (effortSliderOpen && effortOptions.length > 1) ||
|
|
1842
1781
|
(presetPickerOpen && presetOptions.length > 0) || themePickerOpen || historyOpen ||
|
|
1843
1782
|
rewindOpen || searchOpen;
|
|
@@ -1848,11 +1787,16 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1848
1787
|
seekRow(lastUser.id);
|
|
1849
1788
|
else
|
|
1850
1789
|
handle?.scrollToBottom();
|
|
1851
|
-
} })), _jsxs(ScrollBox, { ref: setHandle, flexDirection: "column", flexGrow: 1, flexShrink: 1, stickyScroll: true, children: [_jsx(LogoHeader, { model: channel.model, effort: channel.reasoningEffort, cwd: channel.displayCwd }),
|
|
1790
|
+
} })), _jsxs(ScrollBox, { ref: setHandle, flexDirection: "column", flexGrow: 1, flexShrink: 1, stickyScroll: true, children: [_jsx(LogoHeader, { model: channel.model, effort: channel.reasoningEffort, cwd: channel.displayCwd }), loadedContextVisible && (_jsx(LoadedContextPanel, { context: channel.loadedContext, open: loadedContextOpen, onToggle: () => { setLoadedContextOpen(previous => !previous); } })), _jsx(MessageList, { rows: channel.rows, failureHintRowId: failureHintRowId, failureHint: t('traj-hint-failure', { key: `${modLabel}t` }), expanded: expanded, expandedRows: expandedRows, selectedId: selectionActive ? selectedId : null, onToggleRow: toggleRowExpanded, model: channel.model, showAll: showAllMessages, thinkingVisible: thinkingVisible, onToggleAll: () => { setShowAllMessages(previous => !previous); }, onLoadOlder: () => channel.loadOlder(), registerRowRef: registerRowRef, scrollHandle: handle, forceMountRowId: forceMountRowId, newSinceRowId: isSticky ? null : lastSeenRowIdRef.current, onUnseenCount: setUnseenCount })] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [showPill && (_jsx(NewMessagesPill, { count: unseenCount, onClick: () => handle?.scrollToBottom() })), channel.working &&
|
|
1852
1791
|
(channel.activityEnabled &&
|
|
1853
1792
|
channel.workingActivity !== undefined &&
|
|
1854
1793
|
channel.workingActivity.line !== '' &&
|
|
1855
|
-
channel.workingActivity.phase !== 'idle' ? (_jsx(Box, { marginTop: 1, children: _jsx(ActivityLine, { activity: channel.workingActivity, activityFrames: channel.activityFrames, warnPct: activityWarnPct, warnDanger: activityWarnPct !== undefined && activityWarnPct >= 95,
|
|
1794
|
+
channel.workingActivity.phase !== 'idle' ? (_jsx(Box, { marginTop: 1, children: _jsx(ActivityLine, { activity: channel.workingActivity, activityFrames: channel.activityFrames, warnPct: activityWarnPct, warnDanger: activityWarnPct !== undefined && activityWarnPct >= 95,
|
|
1795
|
+
// Upload = real tokens of the last request; download =
|
|
1796
|
+
// the animated chars/4 estimate, matching the classic
|
|
1797
|
+
// spinner's counter (the suffix used raw chars before,
|
|
1798
|
+
// inflating the reading next to a real upload number).
|
|
1799
|
+
suffix: `${lastUploadTokens > 0 ? ` · ↑ ${formatTokens(lastUploadTokens)}` : ''} · ↓ ${formatTokens(Math.round(channel.responseChars / 4))} tokens` }) })) : (_jsx(WorkingSpinner, { mode: channel.spinnerMode, hasActiveTools: channel.activeToolCount > 0, responseLengthRef: responseLengthRef, uploadTokensRef: uploadTokensRef, loadingStartTimeRef: loadingStartTimeRef, totalPausedMsRef: totalPausedMsRef, pauseStartTimeRef: pauseStartTimeRef, thinkingStatus: thinkingStatus }))), _jsx(GoalTodoPanel, { channel: channel }), approvalSnapshot !== null ? (_jsx(ApprovalPanel, { approval: approvalSnapshot, onDecide: outcome => approvals.decide(outcome) }, approvalSnapshot.key)) : btw !== null ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(BtwPanel, { question: btw.question, answer: btw.answer, error: btw.error, streaming: !btw.done, onClose: closeBtw, onCopy: () => {
|
|
1856
1800
|
void setClipboard(btw.answer ?? '').then(raw => { if (raw)
|
|
1857
1801
|
process.stdout.write(raw); });
|
|
1858
1802
|
channel.notify(t('copied-chars', { n: (btw.answer ?? '').length }), { timeoutMs: 1500 });
|
|
@@ -1862,7 +1806,7 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1862
1806
|
band: wakeBand,
|
|
1863
1807
|
hint: trajectorySeen ? undefined : `${modLabel}t`,
|
|
1864
1808
|
tick: Math.floor(wakeTime / 120),
|
|
1865
|
-
} }), dialogOverlayOpen && (_jsxs(OverlayAbove, { maxHeight: Math.max(terminalRows - 8, 8), children: [thinkingOpen && (_jsx(ThinkingToggle, { currentValue: thinkingVisible, focusIndex: thinkingFocus, confirmationPending: thinkingConfirm })), workspacePickerOpen && workspaceTargets.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(WorkspacePicker, { targets: workspaceTargets, focusIndex: workspaceIndex, currentCwd: channel.cwd }) })), workspaceFlow !== null && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(WorkspaceFlowPicker, { title: workspaceFlow.title, choices: workspaceFlow.choices, focusIndex: workspaceFlowIndex, busy: workspaceFlowBusy, input: workspaceFlowInput }) })),
|
|
1809
|
+
} }), dialogOverlayOpen && (_jsxs(OverlayAbove, { maxHeight: Math.max(terminalRows - 8, 8), children: [thinkingOpen && (_jsx(ThinkingToggle, { currentValue: thinkingVisible, focusIndex: thinkingFocus, confirmationPending: thinkingConfirm })), workspacePickerOpen && workspaceTargets.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(WorkspacePicker, { targets: workspaceTargets, focusIndex: workspaceIndex, currentCwd: channel.cwd }) })), workspaceFlow !== null && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(WorkspaceFlowPicker, { title: workspaceFlow.title, choices: workspaceFlow.choices, focusIndex: workspaceFlowIndex, busy: workspaceFlowBusy, input: workspaceFlowInput }) })), modelPickerOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: models.length === 0 ? (_jsx(ModelPickerLoading, {})) : (_jsx(ModelPicker, { models: models, focusIndex: modelIndex, currentModel: `${channel.provider}/${channel.model}` })) })), activityPickerOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(ActivityPicker, { focusIndex: activityIndex, currentPreset: channel.activityFrames }) })), effortSliderOpen && effortOptions.length > 1 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(EffortSlider, { options: effortOptions, focusIndex: effortIndex, currentId: channel.reasoningEffort }) })), presetPickerOpen && presetOptions.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(PresetPicker, { presets: presetOptions, focusIndex: presetIndex, currentPreset: channel.agentPreset }) })), themePickerOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(ThemePicker, { focusIndex: themeIndex, currentTheme: themeName }) })), historyOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(HistorySearchDialog, { query: historyQuery, cursorOffset: historyCursor, matches: historyMatches, focusIndex: historyFocus }) })), rewindOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(RewindPicker, { rows: rewindRows, focusIndex: rewindIndex, confirmRow: rewindConfirm }) })), searchOpen && _jsx(TranscriptSearchBar, { query: searchQuery, cursorOffset: searchCursor, count: searchCount, current: searchCurrent })] }))] })] }));
|
|
1866
1810
|
}
|
|
1867
1811
|
/**
|
|
1868
1812
|
* The pinned prompt header shown above the ScrollBox while the user has
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Channel } from '../dsh-adapter/channel.js';
|
|
3
|
+
/**
|
|
4
|
+
* The session browser — `/resume` as a screen of its own.
|
|
5
|
+
*
|
|
6
|
+
* The old picker was a panel of eight titles and a timestamp, and the reason
|
|
7
|
+
* it could not be more than that was never layout: the data behind it was five
|
|
8
|
+
* fields wide. With every session now arriving classified, sized, dated and
|
|
9
|
+
* attributed, the surface that shows them can do the job a person actually
|
|
10
|
+
* came for — find one conversation among many.
|
|
11
|
+
*
|
|
12
|
+
* What that means concretely:
|
|
13
|
+
*
|
|
14
|
+
* - Search is always live. There is no mode to enter; typing filters, because
|
|
15
|
+
* the list is the search results.
|
|
16
|
+
* - Delegated sub-agent runs are folded away by default and revealed under
|
|
17
|
+
* their parents on demand. They are not noise to be deleted — they are the
|
|
18
|
+
* model's own work, and it is worth being able to open one — but they are
|
|
19
|
+
* not what "resume a conversation" means, and there are five of them for
|
|
20
|
+
* every conversation.
|
|
21
|
+
* - Sessions that hold no conversation are never listed, only counted, with
|
|
22
|
+
* one action to clear them.
|
|
23
|
+
* - The preview shows the end of a session, so "is this the one I was in the
|
|
24
|
+
* middle of" is answerable without resuming it.
|
|
25
|
+
*
|
|
26
|
+
* Every one of those reads bounded data, so the screen behaves the same on a
|
|
27
|
+
* fifty-session history as on a five-session one.
|
|
28
|
+
*/
|
|
29
|
+
export declare function SessionBrowser({ channel, home, sameProject, onClose, }: {
|
|
30
|
+
channel: Channel;
|
|
31
|
+
/** Home directory, for collapsing project paths to `~`. */
|
|
32
|
+
home: string;
|
|
33
|
+
/** Whether a stored cwd belongs to the same project as the live session. */
|
|
34
|
+
sameProject: (a: string, b: string) => boolean;
|
|
35
|
+
onClose: () => void;
|
|
36
|
+
}): React.ReactNode;
|
|
37
|
+
//# sourceMappingURL=SessionBrowser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionBrowser.d.ts","sourceRoot":"","sources":["../../../src/screens/SessionBrowser.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAsBzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAoDxD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,IAAI,EACJ,WAAW,EACX,OAAO,GACR,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAA;IACZ,4EAA4E;IAC5E,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,CAAA;IAC9C,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,GAAG,KAAK,CAAC,SAAS,CAselB"}
|