@agi-fans/dsh-tui 0.23.1 → 0.24.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.
@@ -414,6 +414,9 @@ export class LocalTui {
414
414
  #mermaidMode = 'auto';
415
415
  /** Transcript collapses every thought to one line; Ctrl+T flips it. */
416
416
  #hideThinking = true;
417
+ #showFileChanges = true;
418
+ #workspaceReview;
419
+ #workspacePress;
417
420
  #mouseInteraction = 'auto';
418
421
  #tmuxMouseEnabled;
419
422
  #disclosurePress;
@@ -598,6 +601,7 @@ export class LocalTui {
598
601
  // live window so it stays anchored at the bottom.
599
602
  this.#selection.clear();
600
603
  this.#mouseActionPress = undefined;
604
+ this.#workspacePress = undefined;
601
605
  this.#jumpPress = undefined;
602
606
  this.#disclosurePress = undefined;
603
607
  const repaint = () => {
@@ -859,6 +863,7 @@ export class LocalTui {
859
863
  && this.#inspected?.writable === inspected?.writable)
860
864
  return;
861
865
  if (this.#inspected?.id !== inspected?.id) {
866
+ this.#workspacePress = undefined;
862
867
  this.#fileReviewAbort?.abort();
863
868
  this.#closeFileBrowser();
864
869
  }
@@ -886,6 +891,7 @@ export class LocalTui {
886
891
  async reviewFiles(request) {
887
892
  if (!this.#tty || this.#disposed || request.signal.aborted)
888
893
  return;
894
+ this.#workspacePress = undefined;
889
895
  this.#flushPasteBurst(true);
890
896
  this.#pasteBurst.clear();
891
897
  this.#finishPrompt(null);
@@ -1220,7 +1226,8 @@ export class LocalTui {
1220
1226
  const browserState = promptBrowserState(request);
1221
1227
  request = localizePrompt(request, resolveUiLanguage(this.#uiLanguage));
1222
1228
  // Background approvals take the terminal from a user-opened file reader.
1223
- if (this.#fileReviewAbort !== undefined && request.signal !== this.#fileReviewAbort.signal)
1229
+ if (this.#fileReviewAbort !== undefined && request.signal !== this.#fileReviewAbort.signal
1230
+ && request.signal !== this.#fileBrowser?.request.signal)
1224
1231
  this.#fileReviewAbort.abort();
1225
1232
  if (this.#disposed || request.signal?.aborted === true)
1226
1233
  return Promise.resolve(null);
@@ -1251,6 +1258,7 @@ export class LocalTui {
1251
1258
  this.#promptChoicePress = undefined;
1252
1259
  this.#disclosurePress = undefined;
1253
1260
  this.#mouseActionPress = undefined;
1261
+ this.#workspacePress = undefined;
1254
1262
  this.#jumpPress = undefined;
1255
1263
  this.#wheelGesture = IDLE_WHEEL;
1256
1264
  const savedInput = this.#editor.snapshot();
@@ -1341,6 +1349,7 @@ export class LocalTui {
1341
1349
  });
1342
1350
  }
1343
1351
  replaceSession(events, presentations, status = 'idle', reason = 'open') {
1352
+ this.#workspacePress = undefined;
1344
1353
  if (reason !== 'refresh') {
1345
1354
  this.#fileReviewAbort?.abort();
1346
1355
  this.#closeFileBrowser();
@@ -1372,6 +1381,7 @@ export class LocalTui {
1372
1381
  }
1373
1382
  setSession(info) {
1374
1383
  if (info.id !== this.#sessionId) {
1384
+ this.#workspacePress = undefined;
1375
1385
  this.#fileReviewAbort?.abort();
1376
1386
  this.#closeFileBrowser();
1377
1387
  }
@@ -1456,6 +1466,7 @@ export class LocalTui {
1456
1466
  mathMode: this.#mathMode,
1457
1467
  mermaidMode: this.#mermaidMode,
1458
1468
  hideThinking: this.#hideThinking,
1469
+ showFileChanges: this.#showFileChanges,
1459
1470
  editor: this.#externalEditor,
1460
1471
  terminalTitle: this.#terminalTitle,
1461
1472
  terminalState: this.#terminalState,
@@ -1743,6 +1754,15 @@ export class LocalTui {
1743
1754
  return pending.queued.length === 0 && pending.sending.size === 0
1744
1755
  && this.#state.nextTurnInbox.length === 0 && !this.#state.nextStepInbox.some(message => message.source.kind === 'user');
1745
1756
  }
1757
+ setWorkspaceReviewHandler(handler) {
1758
+ this.#workspaceReview = handler;
1759
+ return () => {
1760
+ if (this.#workspaceReview === handler) {
1761
+ this.#workspaceReview = undefined;
1762
+ this.#workspacePress = undefined;
1763
+ }
1764
+ };
1765
+ }
1746
1766
  setGoalHandler(handler) {
1747
1767
  this.#goalHandler = handler;
1748
1768
  this.#syncSettingsActions();
@@ -2161,6 +2181,7 @@ export class LocalTui {
2161
2181
  mathMode: this.#mathMode,
2162
2182
  mermaidMode: this.#mermaidMode,
2163
2183
  hideThinking: true,
2184
+ showFileChanges: this.#showFileChanges,
2164
2185
  height: this.#term.height(),
2165
2186
  model: this.#model,
2166
2187
  ...(this.#reasoningEffort === undefined ? {} : { reasoningEffort: this.#reasoningEffort }),
@@ -2286,7 +2307,8 @@ export class LocalTui {
2286
2307
  return Math.max(1, this.#scrollBudget > 2 ? this.#scrollBudget - 2 : 1);
2287
2308
  }
2288
2309
  #scrollBy(delta) {
2289
- if (this.#follow && delta > 0)
2310
+ this.#workspacePress = undefined;
2311
+ if (this.#follow && (delta > 0 || this.#maxStart === 0))
2290
2312
  return;
2291
2313
  if (delta === 0 && this.#maxStart === 0)
2292
2314
  return;
@@ -2349,6 +2371,7 @@ export class LocalTui {
2349
2371
  enabled = enabled && this.#mouseInteraction !== 'native'
2350
2372
  && (this.#mouseInteraction === 'tui' || this.#tmuxMouseEnabled !== false);
2351
2373
  if (!enabled) {
2374
+ this.#workspacePress = undefined;
2352
2375
  this.#mouseActionPress = undefined;
2353
2376
  this.#selection.clear();
2354
2377
  this.#jumpPress = undefined;
@@ -2689,6 +2712,18 @@ export class LocalTui {
2689
2712
  return { row, column: tool.actionColumn, width: tool.key.startsWith('output:') ? 6 : 5, key: tool.key, action: 'tool', block: tool.block };
2690
2713
  return undefined;
2691
2714
  }
2715
+ #fileRegionAt(row) {
2716
+ const document = this.#renderer.documentRowAt(row);
2717
+ const frame = this.#fileActionFrame;
2718
+ const source = frame?.documentRows;
2719
+ const region = document === undefined ? undefined : frame?.transcript?.fileRegions?.find(region => document >= region.start && document < region.end);
2720
+ const expected = document === undefined || source === undefined || source === null ? undefined
2721
+ : frame?.lines[source.frameStart + document - source.documentStart];
2722
+ const actual = this.#renderer.viewport()[row];
2723
+ // Committed native history can differ from the live document after a reflow.
2724
+ return expected !== undefined && actual !== undefined
2725
+ && stripAnsi(sanitizeDisplayLine(expected)).trimEnd() === stripAnsi(actual).trimEnd() ? region : undefined;
2726
+ }
2692
2727
  #handleMouse(event) {
2693
2728
  if (!this.#mouseTracking || this.#paste || this.#pasteInFlight > 0)
2694
2729
  return;
@@ -2765,6 +2800,7 @@ export class LocalTui {
2765
2800
  const target = this.#mouseActionAt(event.row, event.column);
2766
2801
  if (event.action === 'release') {
2767
2802
  this.#mouseActionPress = undefined;
2803
+ this.#workspacePress = undefined;
2768
2804
  if (event.button === 'left' && !event.shift && target?.key === pressedAction.key && target.row === pressedAction.row && target.column === pressedAction.column) {
2769
2805
  if (target.action === 'copy')
2770
2806
  void this.#copySelection(true);
@@ -2795,21 +2831,14 @@ export class LocalTui {
2795
2831
  }
2796
2832
  const inside = (target) => event.row === target.row && event.column >= target.column && event.column < target.column + target.width;
2797
2833
  if (event.button === 'right') {
2834
+ this.#workspacePress = undefined;
2798
2835
  this.#disclosurePress = undefined;
2799
2836
  if (event.action === 'press') {
2800
2837
  if (this.#selection.active)
2801
2838
  void this.#copySelection(true);
2802
2839
  else {
2803
- const document = this.#renderer.documentRowAt(event.row);
2804
- const frame = this.#fileActionFrame;
2805
- const source = frame?.documentRows;
2806
- const region = document === undefined ? undefined : frame?.transcript?.fileRegions?.find(region => document >= region.start && document < region.end);
2807
- const expected = document === undefined || source === undefined || source === null ? undefined
2808
- : frame?.lines[source.frameStart + document - source.documentStart];
2809
- const actual = this.#renderer.viewport()[event.row];
2810
- // Native-history snapshots may differ from the current document. Do
2811
- // not reinterpret their old text as another file after a reflow.
2812
- if (region !== undefined && expected !== undefined && actual !== undefined && stripAnsi(sanitizeDisplayLine(expected)).trimEnd() === stripAnsi(actual).trimEnd()) {
2840
+ const region = this.#fileRegionAt(event.row);
2841
+ if (region !== undefined) {
2813
2842
  const block = this.#state.blocks[region.block];
2814
2843
  const action = block === undefined ? undefined : blockFileAction(block);
2815
2844
  if (action !== undefined)
@@ -2820,6 +2849,10 @@ export class LocalTui {
2820
2849
  return;
2821
2850
  }
2822
2851
  if (event.action === 'press') {
2852
+ const region = !event.shift && this.#workspaceReview !== undefined ? this.#fileRegionAt(event.row) : undefined;
2853
+ const source = region === undefined ? undefined : this.#state.blocks[region.block];
2854
+ this.#workspacePress = source?.kind === 'workspace' && region !== undefined
2855
+ ? { row: event.row, column: event.column, block: region.block, source, ...(region.file === undefined ? {} : { file: region.file }) } : undefined;
2823
2856
  const disclosure = event.shift ? undefined : this.#disclosureAt(event.row, event.column);
2824
2857
  this.#disclosurePress = disclosure === undefined ? undefined : { target: disclosure, row: event.row, column: event.column };
2825
2858
  const target = this.#jumpToLatest;
@@ -2841,6 +2874,9 @@ export class LocalTui {
2841
2874
  }
2842
2875
  }
2843
2876
  else if (event.action === 'drag') {
2877
+ const pressed = this.#workspacePress;
2878
+ if (pressed !== undefined && (event.shift || event.row !== pressed.row || Math.abs(event.column - pressed.column) > 1))
2879
+ this.#workspacePress = undefined;
2844
2880
  // Motion confined to the empty gutter is click jitter. Once it leaves,
2845
2881
  // selection owns the gesture even if the pointer returns to the arrow.
2846
2882
  if (event.shift || this.#pressedDisclosureAt(event.row, event.column) === undefined) {
@@ -2849,6 +2885,15 @@ export class LocalTui {
2849
2885
  }
2850
2886
  }
2851
2887
  else {
2888
+ const pressed = this.#workspacePress;
2889
+ this.#workspacePress = undefined;
2890
+ const region = pressed === undefined ? undefined : this.#fileRegionAt(event.row);
2891
+ if (!event.shift && pressed !== undefined && event.row === pressed.row && Math.abs(event.column - pressed.column) <= 1
2892
+ && region?.block === pressed.block && region.file === pressed.file && this.#state.blocks[pressed.block] === pressed.source) {
2893
+ this.#selection.clear();
2894
+ this.#openWorkspaceReview(pressed.source, pressed.file);
2895
+ return;
2896
+ }
2852
2897
  const target = this.#pressedDisclosureAt(event.row, event.column);
2853
2898
  this.#disclosurePress = undefined;
2854
2899
  if (!event.shift && target !== undefined) {
@@ -3041,6 +3086,20 @@ export class LocalTui {
3041
3086
  this.#fileReviewAbort = undefined;
3042
3087
  }
3043
3088
  }
3089
+ #openWorkspaceReview(block, file) {
3090
+ const handler = this.#workspaceReview;
3091
+ const sessionId = this.#inspected?.id ?? this.#sessionId;
3092
+ if (handler === undefined || sessionId === undefined || block.kind !== 'workspace' || !this.#tty || this.#disposed
3093
+ || this.#prompt !== null || this.#fileReviewAbort !== undefined)
3094
+ return;
3095
+ const abort = new AbortController();
3096
+ this.#fileReviewAbort = abort;
3097
+ void handler({ sessionId, turn: block.turn, ...(file === undefined ? {} : { file }), signal: abort.signal })
3098
+ .catch(error => { if (!abort.signal.aborted && !this.#disposed)
3099
+ this.notice(error instanceof Error ? error.message : String(error), { level: 'error' }); })
3100
+ .finally(() => { if (this.#fileReviewAbort === abort)
3101
+ this.#fileReviewAbort = undefined; });
3102
+ }
3044
3103
  #openFileAction(action, initial) {
3045
3104
  if (!this.#tty || this.#disposed || this.#prompt !== null || this.#fileReviewAbort !== undefined)
3046
3105
  return;
@@ -3092,6 +3151,7 @@ export class LocalTui {
3092
3151
  this.#jumpPress = undefined;
3093
3152
  this.#disclosurePress = undefined;
3094
3153
  this.#mouseActionPress = undefined;
3154
+ this.#workspacePress = undefined;
3095
3155
  if (this.#keyTest !== undefined && this.#prompt?.request.signal === this.#keyTest.signal
3096
3156
  && (event.type === 'text' || event.type === 'paste-start' || event.type === 'paste-end' || event.type === 'key' && !['escape', 'ctrl+c'].includes(event.id))) {
3097
3157
  this.#keyTest.observe(event);
@@ -3922,6 +3982,7 @@ export class LocalTui {
3922
3982
  const previous = options.persist ? this.prefs() : undefined;
3923
3983
  const version = this.#prefsVersion;
3924
3984
  const previousMotion = this.#motion;
3985
+ this.#workspacePress = undefined;
3925
3986
  this.#uiLanguage = prefs.uiLanguage ?? 'auto';
3926
3987
  if (this.#prompt !== null)
3927
3988
  this.#prompt = { ...this.#prompt, request: localizePrompt(this.#prompt.sourceRequest, resolveUiLanguage(this.#uiLanguage)) };
@@ -3940,6 +4001,7 @@ export class LocalTui {
3940
4001
  this.#mathMode = prefs.mathMode ?? 'auto';
3941
4002
  this.#mermaidMode = prefs.mermaidMode ?? 'auto';
3942
4003
  this.#hideThinking = prefs.hideThinking !== false;
4004
+ this.#showFileChanges = prefs.showFileChanges !== false;
3943
4005
  this.#mouseInteraction = prefs.mouseInteraction ?? 'auto';
3944
4006
  this.#checkUpdates = prefs.checkUpdates ?? true;
3945
4007
  this.#startupChangelog = prefs.startupChangelog ?? 'summary';
@@ -4923,17 +4985,17 @@ export class LocalTui {
4923
4985
  }
4924
4986
  #settingsActions() {
4925
4987
  const runtimeActions = [
4926
- { id: 'command:model', behavior: 'Session + Profile · Choose scope in Model', group: 'Models', label: 'Model and reasoning', description: 'Open /model; changes the current selection and saves the default route.', value: [this.#model, this.#reasoningEffort].filter(Boolean).join(' · '), values: [], action: 'model' },
4927
- { id: 'command:permission', behavior: 'Current session · Applies now', group: 'General', label: 'Current session access', description: 'Changes only this session. Default access controls future sessions.', value: this.#sessionControls?.permission ?? 'Choose', values: [], action: 'permission' },
4928
- { id: 'command:agent', behavior: 'Current session · Before the first prompt', group: 'Agent presets', label: 'Current session preset', description: 'Choose before the first prompt; does not change the new-session default.', value: this.#sessionControls?.agentPreset === undefined ? 'Choose' : formatAgentPreset(this.#sessionControls.agentPreset), values: [], action: 'agent' },
4929
- { id: 'command:login', behavior: 'Provider credentials', group: 'Models', label: 'Provider credentials', description: 'Sign in or configure a provider. Does not change the selected model.', value: 'Configure', values: [], action: 'login' },
4930
- { id: 'command:logout', behavior: 'Provider credentials', group: 'Models', label: 'Sign out', description: 'Remove saved provider credentials; environment credentials remain available.', value: 'Choose provider', values: [], action: 'logout' },
4931
- { id: 'command:plugins', group: 'Plugins', label: 'Manage plugins', description: 'Inspect installed entries, enable bundles, discover plugins or start Creator.', value: 'Open', values: [], action: 'plugins' },
4988
+ { id: 'command:model', behavior: 'Session + Profile · Choose scope in Model', group: 'Models', subgroup: 'Model selection', label: 'Model and reasoning', description: 'Choose the current model and reasoning effort. Save as the default or use for this session.', value: [this.#model, this.#reasoningEffort].filter(Boolean).join(' · '), values: [], action: 'model' },
4989
+ { id: 'command:permission', behavior: 'Current session · Applies now', group: 'General', subgroup: 'Session controls', label: 'Current session access', description: 'Changes only this session. Default access controls future sessions.', value: this.#sessionControls?.permission ?? 'Choose', values: [], action: 'permission' },
4990
+ { id: 'command:agent', behavior: 'Current session · Before the first prompt', group: 'Agent presets', subgroup: 'Current session', label: 'Current session preset', description: 'Choose before the first prompt; does not change the new-session default.', value: this.#sessionControls?.agentPreset === undefined ? 'Choose' : formatAgentPreset(this.#sessionControls.agentPreset), values: [], action: 'agent' },
4991
+ { id: 'command:login', behavior: 'Accounts and API keys', group: 'Models', subgroup: 'Accounts and API keys', label: 'Sign in / add API key', description: 'Sign in to a provider account or save an API key.', keywords: ['Provider credentials', 'login', 'API key', 'OAuth'], value: 'Choose provider', values: [], action: 'login' },
4992
+ { id: 'command:logout', behavior: 'Accounts and API keys', group: 'Models', subgroup: 'Accounts and API keys', label: 'Sign out / remove key', description: 'Choose a provider or account to sign out or remove locally saved credentials.', keywords: ['logout', 'credentials'], value: 'Choose provider', values: [], action: 'logout' },
4993
+ { id: 'command:plugins', group: 'Plugins', subgroup: 'Plugin management', label: 'Manage plugins', description: 'Inspect installed entries, enable bundles, discover plugins or start Creator.', value: 'Open', values: [], action: 'plugins' },
4932
4994
  ].filter(item => this.#runtimeCommands.some(command => command.name === item.action));
4933
4995
  const customBindings = Object.entries(this.#keybindings).filter(([key, action]) => DEFAULT_KEYBINDINGS[key] !== action).length;
4934
4996
  return [
4935
4997
  ...runtimeActions,
4936
- ...(this.#goalHandler === undefined ? [] : [{ id: 'action:goal', behavior: 'Current session · Applies now', group: 'General', label: 'Current goal', description: 'Edit, pause, resume or clear this session’s goal.', value: 'Open', values: [], action: 'manage-goal' }]),
4998
+ ...(this.#goalHandler === undefined ? [] : [{ id: 'action:goal', behavior: 'Current session · Applies now', group: 'General', subgroup: 'Session controls', label: 'Current goal', description: 'Edit, pause, resume or clear this session’s goal.', value: 'Open', values: [], action: 'manage-goal' }]),
4937
4999
  ...(this.#keybindingsPath === undefined ? [] : [{
4938
5000
  id: 'action:keybindings',
4939
5001
  label: 'Keybindings',
@@ -5852,6 +5914,7 @@ export class LocalTui {
5852
5914
  toolsExpanded: this.#toolsExpanded,
5853
5915
  expandedOutputs: this.#expandedOutputs,
5854
5916
  hideThinking: true,
5917
+ showFileChanges: this.#showFileChanges,
5855
5918
  expandedTools: this.#detailedToolIds(),
5856
5919
  expandedReasoning: this.#visibleReasoning(),
5857
5920
  ...(this.#openedGroups === undefined ? {} : { openedGroups: this.#openedGroups }),
@@ -5962,6 +6025,7 @@ export function apply(ctx, config) {
5962
6025
  motion: config.motion.get(),
5963
6026
  mathMode: config.mathMode.get(),
5964
6027
  mermaidMode: config.mermaidMode.get(),
6028
+ showFileChanges: config.showFileChanges.get(),
5965
6029
  editor: config.editor.get(),
5966
6030
  terminalTitle: config.terminalTitle.get(),
5967
6031
  terminalState: config.terminalState.get(),