@toddzheng024/dscode-bundle 0.7.13 → 0.7.15

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.
@@ -29,9 +29,8 @@
29
29
  You are a coding agent powered by the {{model}} model, working through a persistent shell.
30
30
  Reply in the language the user writes in.
31
31
  Keep the user informed during tool-driven work with ordinary assistant text, visible even when verbose mode is off. Reasoning blocks and tool descriptions do not count as progress updates.
32
- Before the first tool call, briefly state what you will check or change. For a simple answer without tools, answer directly.
33
- During sustained work, give a concise progress update roughly every minute when you have control, and when a significant finding, blocker or change of direction occurs. If a long tool call prevents an update, summarize its result when control returns; do not claim to speak while a tool is blocking.
34
- Use one or two sentences explaining what you learned and what the next step will resolve. Report observable facts and decisions, not private reasoning, raw tool payloads or a narration of every command. Avoid repetitive filler and invented progress.
32
+ Before the first tool call, briefly state what you will check or change; for a simple answer without tools, answer directly.
33
+ During sustained work, send an update when you have something to report: a significant finding, a blocker, a change of direction, or the result of a long blocking call once control returns. Do not announce routine reads, do not claim to speak while a tool is blocking, and never pad: an update says what changed and what the next step resolves, in one or two sentences, using observable facts and decisions rather than private reasoning, raw tool payloads or a narration of every command.
35
34
  Continue working after each update without asking permission for already-authorized steps. Progress text is not the final answer; finish with a self-contained result and any remaining gaps.
36
35
  Before changing code, read the relevant code and any project instructions; reuse existing functions and patterns instead of adding new machinery.
37
36
  Make routine judgment calls yourself and ask only when different answers would lead to materially different work.
@@ -48,6 +47,10 @@
48
47
  - id: agent-instructions
49
48
  name: '@deepseek-ai/dsh-agent-instructions'
50
49
  config:
50
+ # The launcher folds AGENTS.md/CLAUDE.md from directories above the project
51
+ # root, up to home, in front of the user-global file; without those files it
52
+ # points at $DSH_HOME, exactly as before.
53
+ dshHome: !!js 'process.env.DSCODE_INSTRUCTION_HOME'
51
54
  maxBytes: 65536
52
55
 
53
56
  - id: persistent-shell
@@ -131,6 +134,10 @@
131
134
  # carries whatever the deployment registered globally (repository plugins).
132
135
  - id: skill-filesystem
133
136
  name: '@deepseek-ai/dsh-skill-filesystem'
137
+ config:
138
+ # DSCODE_SKILL_ANCESTORS=1 adds the skill roots of directories between the
139
+ # project root and home at rank 300: headers enter the catalog, bodies stay lazy.
140
+ customSkillDirs: !!js 'JSON.parse(process.env.DSCODE_SKILL_ANCESTOR_DIRS ?? "[]")'
134
141
 
135
142
  - id: tool-skill
136
143
  name: '@deepseek-ai/dsh-tool-skill'
@@ -311,18 +318,3 @@
311
318
  - id: present
312
319
  name: '@deepseek-ai/dsh-tool-present'
313
320
 
314
- # The TUI composes this standing preset on the first message. Connecting Chrome
315
- # here keeps the prompt-free welcome screen off the MCP startup path; the first
316
- # agent request still waits for the tool catalog before reaching the model.
317
- - id: mcp-chrome
318
- name: '@deepseek-ai/dsh-mcp-client'
319
- inject: [dscodePaths]
320
- config:
321
- serverName: chrome
322
- transport: stdio
323
- command: !!js process.execPath
324
- args: !!js "[ctx.dscodePaths.chrome, '--isolated', '--no-usage-statistics', '--no-performance-crux']"
325
- env:
326
- CHROME_DEVTOOLS_MCP_NO_UPDATE_CHECKS: '1'
327
- failOnStartupError: true
328
- toolCallTimeoutMs: 60000
@@ -1,5 +1,6 @@
1
+ // dscode-compaction-prefetch-v1
1
2
  // dscode-compaction-threshold-v1
2
- import { pricedCompactionPolicy as dscodePricedCompactionPolicy } from "../../plugins/compaction/threshold.mjs";
3
+ import { prefetchThresholdTokens as dscodePrefetchThresholdTokens, pricedCompactionPolicy as dscodePricedCompactionPolicy } from "../../plugins/compaction/threshold.mjs";
3
4
  import z from "@deepseek-ai/schemastery";
4
5
  import { CompactionEngine, CompactionId, ManualCompactionError, compactCheckpointSource, toolPairingBalancedAfter, toolPairingBalancedBefore } from "@deepseek-ai/dsh-compaction";
5
6
  import { BlockAssembler, CONTEXT_WINDOW_EXCEEDED_CODE, LlmError, contentHasImage, createUserMessage, errorChain } from "@deepseek-ai/dsh-llm";
@@ -900,6 +901,7 @@ var BasicCompactionEngine = class extends CompactionEngine {
900
901
  const targetKey = `${target.provider}/${target.model}`;
901
902
  if (context === void 0) throw new TargetPressureConfigError(targetKey, `compaction-basic: no context capacity for ${targetKey}; configure contextWindow on that adapter model`);
902
903
  const spec = resolveCompactSpec(await dscodePricedCompactionPolicy(this.config, policy), context.contextWindow);
904
+ this.dscodePlanPrefetch(agent, measurement, spec, context.contextWindow, signal);
903
905
  if (measurement.totalTokens < spec.thresholdTokens) return null;
904
906
  if (prune !== void 0) {
905
907
  prune.pruneSession(agent.session);
@@ -907,6 +909,12 @@ var BasicCompactionEngine = class extends CompactionEngine {
907
909
  }
908
910
  if (measurement.totalTokens < spec.thresholdTokens) return null;
909
911
  let result = null;
912
+ const dscodePrefetched = await this.dscodeCommitPrefetch(agent);
913
+ if (dscodePrefetched !== null) {
914
+ result = dscodePrefetched;
915
+ measurement = meter.measure(agent.session);
916
+ if (measurement.totalTokens < spec.thresholdTokens) return result;
917
+ }
910
918
  for (let attempt = 0; attempt <= spec.compactionRetries; attempt += 1) {
911
919
  const range = selectCompactableRange(agent.session, measurement, spec.retainTokens);
912
920
  if (range === null) {
@@ -971,6 +979,88 @@ var BasicCompactionEngine = class extends CompactionEngine {
971
979
  throw new ManualCompactionError("busy", "manual compaction requires an idle agent with no waking queued work", { cause: error });
972
980
  }
973
981
  }
982
+ /** dscode: the one background prefetch this engine may hold per session. */
983
+ dscodePrefetch = /* @__PURE__ */ new WeakMap();
984
+ /**
985
+ * dscode: start one background summarization of the oldest compactable span once the
986
+ * measured pressure crosses the prefetch mark, one lead below the priced threshold.
987
+ * Nothing is appended and no compaction lock is taken, so an invalidated or cancelled
988
+ * prefetch costs only the summarization call.
989
+ * @param agent - agent whose pressure the caller just priced.
990
+ * @param measurement - the measurement the caller priced.
991
+ * @param spec - the caller's resolved spec for this route.
992
+ * @param contextWindow - the route's context window.
993
+ * @param signal - live turn signal forwarded to the summarizer.
994
+ */
995
+ dscodePlanPrefetch(agent, measurement, spec, contextWindow, signal) {
996
+ const session = agent.session;
997
+ const existing = this.dscodePrefetch.get(session);
998
+ if (existing !== void 0) {
999
+ if (existing.failure === null) return;
1000
+ this.dscodePrefetch.delete(session);
1001
+ }
1002
+ if (measurement.totalTokens >= spec.thresholdTokens) return;
1003
+ if (measurement.totalTokens < dscodePrefetchThresholdTokens(spec.thresholdTokens, contextWindow)) return;
1004
+ if (inspectCompactionEntryState(session).unmatchedCompactionStart !== void 0) return;
1005
+ const range = selectCompactableRange(session, measurement, spec.retainTokens);
1006
+ if (range === null) return;
1007
+ let selection;
1008
+ try {
1009
+ selection = validateSurfaceRegion(session, range.start, range.end);
1010
+ } catch (error) {
1011
+ return;
1012
+ }
1013
+ const prefetch = { compactionId: CompactionId(randomUUID()), summarized: null, failure: null, wait: null };
1014
+ this.dscodePrefetch.set(session, prefetch);
1015
+ prefetch.wait = (async () => {
1016
+ try {
1017
+ prefetch.summarized = await summarizeCompaction(this.regionDependencies(), prepareCompaction(this.regionDependencies(), session, selection), agent, prefetch.compactionId, void 0, signal);
1018
+ } catch (error) {
1019
+ prefetch.failure = error;
1020
+ }
1021
+ })();
1022
+ }
1023
+ /**
1024
+ * dscode: commit the pending prefetch, or return null so the caller summarizes afresh.
1025
+ * A finished summary is validated before the durable marker opens; one still running
1026
+ * opens the marker first, so its wait carries the ordinary compaction indicator.
1027
+ * Commit replaces the prefetched span with its checkpoint and leaves everything
1028
+ * appended after it verbatim.
1029
+ * @param agent - agent whose pressure triggered the automatic compaction.
1030
+ * @returns the committed compaction result, or null when no prefetch was usable.
1031
+ */
1032
+ async dscodeCommitPrefetch(agent) {
1033
+ const session = agent.session;
1034
+ const prefetch = this.dscodePrefetch.get(session);
1035
+ if (prefetch === void 0) return null;
1036
+ this.dscodePrefetch.delete(session);
1037
+ if (prefetch.failure !== null) return null;
1038
+ const dependencies = this.regionDependencies();
1039
+ if (prefetch.summarized !== null) try {
1040
+ assertSelectedSpanStable(dependencies, session, prefetch.summarized);
1041
+ } catch (error) {
1042
+ return null;
1043
+ }
1044
+ const openTurn = inspectCompactionEntryState(session).openTurn;
1045
+ const lifecycle = {
1046
+ compactionId: prefetch.compactionId,
1047
+ ...openTurn === null ? {} : { turn: openTurn }
1048
+ };
1049
+ const startEvent = session.append("compaction/start", lifecycle);
1050
+ let closing = false;
1051
+ try {
1052
+ await prefetch.wait;
1053
+ if (prefetch.failure !== null) throw prefetch.failure;
1054
+ assertSelectedSpanStable(dependencies, session, prefetch.summarized);
1055
+ const pending = commitCompactionBody(session, startEvent, prefetch.summarized);
1056
+ closing = true;
1057
+ const endEvent = session.append("compaction/end", lifecycle);
1058
+ return completeCompaction(pending, endEvent);
1059
+ } catch (error) {
1060
+ if (!closing) session.append("compaction/end", { ...lifecycle, error: errorChain(error) });
1061
+ return null;
1062
+ }
1063
+ }
974
1064
  /** Bind the effective token meter and dynamically dispatched summarizer hook. */
975
1065
  regionDependencies() {
976
1066
  return {
@@ -1,3 +1,4 @@
1
+ // dscode-shell-capture-v1
1
2
  // dscode-stdin-stall-v2
2
3
  const STDIN_STALL_MS = 5000;
3
4
  const STDIN_STALL_NOTE = "\n[Interrupted after 5s with no output: the command held the terminal without producing anything, and this tool cannot supply terminal input. Give it stdin (a heredoc, `< file` or `< /dev/null`) or use a non-interactive form.]";
@@ -174,7 +175,8 @@ function renderShellExitStatus(content, exitCode, signal) {
174
175
  * @returns the complete model-facing result.
175
176
  */
176
177
  async function respondToSessionExit(ctx, shells, owner, id, status, marker, fallback, fallbackTruncated, config) {
177
- const snapshot = retainedScrollback(ctx, owner, id);
178
+ const window = ctx.terminals.read(owner, id, { capture: true });
179
+ const snapshot = window.text.length > 0 ? window : retainedScrollback(ctx, owner, id);
178
180
  await shells.reset(owner, "persistent bash shell exited");
179
181
  return [renderShellExitStatus(renderCaptured(partialOutput(snapshot, marker, fallback, fallbackTruncated), config.maxOutputChars), status.exitCode, status.signal), SHELL_RESET_MESSAGE].filter((part) => part.length > 0).join("\n");
180
182
  }
@@ -272,6 +274,7 @@ async function executeCommand(ctx, shells, owner, command, config, upstream) {
272
274
  try {
273
275
  operation = ctx.terminals.startSend(owner, id, {
274
276
  text: first ? wrapped : "",
277
+ capture: first,
275
278
  submit: first,
276
279
  signal: commandDeadline.signal
277
280
  });
@@ -284,13 +287,11 @@ async function executeCommand(ctx, shells, owner, command, config, upstream) {
284
287
  const incremental = operation.readOutput();
285
288
  fallback = incremental.delta.length > 0 ? fallback + incremental.delta : result.viewport;
286
289
  fallbackTruncated ||= incremental.truncated || result.truncated;
287
- const latest = ctx.terminals.read(owner, id, {
288
- offset: 0,
289
- count: SCROLLBACK_PAGE_LINES
290
- });
290
+ const captured = ctx.terminals.read(owner, id, { capture: true });
291
+ fallbackTruncated ||= captured.truncated;
291
292
  const timedOut = timeoutOf(commandDeadline.signal, TIMEOUT_CODE);
292
293
  if (timedOut !== void 0) {
293
- const partial = renderCaptured(partialOutput(retainedScrollback(ctx, owner, id, latest), marker, fallback, fallbackTruncated), config.maxOutputChars);
294
+ const partial = renderCaptured(partialOutput(captured, marker, fallback, fallbackTruncated), config.maxOutputChars);
294
295
  await shells.reset(owner, "persistent bash command timed out");
295
296
  return [
296
297
  `Your command timed out after ${Math.round(timedOut.timeoutMs / 1e3)} seconds or experienced an OOM error. Below is partial output:`,
@@ -302,12 +303,12 @@ async function executeCommand(ctx, shells, owner, command, config, upstream) {
302
303
  await shells.reset(owner, "persistent bash command aborted");
303
304
  commandDeadline.signal.throwIfAborted();
304
305
  }
305
- if (latest.text.includes(marker.end)) {
306
- const complete = commandOutput(retainedScrollback(ctx, owner, id, latest), marker);
306
+ if (captured.text.includes(marker.end)) {
307
+ const complete = commandOutput(captured, marker);
307
308
  if (complete !== void 0) return renderCaptured(complete, config.maxOutputChars) + (stdinStalled ? STDIN_STALL_NOTE : "");
308
309
  }
309
310
  if (result.sessionStatus.kind === "exited") return await respondToSessionExit(ctx, shells, owner, id, result.sessionStatus, marker, fallback, fallbackTruncated, config);
310
- const partial = renderCaptured(partialOutput(retainedScrollback(ctx, owner, id, latest), marker, fallback, fallbackTruncated), config.maxOutputChars);
311
+ const partial = renderCaptured(partialOutput(captured, marker, fallback, fallbackTruncated), config.maxOutputChars);
311
312
  if (result.waitReason === "stdin_read") stdinWaited = true;
312
313
  if (result.waitReason === "stdin_read" && partial.length > 0) return partial;
313
314
  if (stdinWaited && partial.length === 0 && !stdinStalled && Date.now() - startedAt >= STDIN_STALL_MS) {
@@ -1,3 +1,4 @@
1
+ // dscode-shell-capture-v1
1
2
  // dscode-no-history-expansion-v1
2
3
  import { createRequire } from "node:module";
3
4
  import { TerminalBackendCleanupError, TerminalError } from "@deepseek-ai/dsh-terminal";
@@ -254,6 +255,7 @@ function utf8Tail(text, maxBytes) {
254
255
  truncated: true
255
256
  };
256
257
  }
258
+ const CAPTURE_MAX_BYTES = 4194304;
257
259
  var BoundedTextBuffer = class {
258
260
  maxBytes;
259
261
  maxLines;
@@ -454,6 +456,7 @@ var LocalPtySession = class {
454
456
  if (this.statusValue.kind === "exited") throw new Error("PTY session has exited");
455
457
  if (this.active !== void 0) throw new TerminalError(`PTY session already has an active send${this.activeWrite !== void 0 ? " or draining provider write" : this.interrupting !== void 0 ? " or draining foreground interrupt" : ""}`, "SEND_ACTIVE");
456
458
  if (request.signal?.aborted === true) throw new Error("PTY send aborted before write");
459
+ if (request.capture === true) this.capture = new BoundedTextBuffer(CAPTURE_MAX_BYTES);
457
460
  const operation = new LocalSendOperation(this.config.maxReadBytes, Date.now(), () => {
458
461
  this.interrupt(operation);
459
462
  });
@@ -520,6 +523,7 @@ var LocalPtySession = class {
520
523
  this.promptTail = "";
521
524
  }
522
525
  read(request) {
526
+ if (request.capture === true) return this.capture?.snapshot() ?? { text: "", truncated: false };
523
527
  const snapshot = this.scrollback.snapshot();
524
528
  const lines = snapshot.text.split("\n");
525
529
  const totalLines = snapshot.text.length === 0 ? 0 : lines.length;
@@ -625,6 +629,7 @@ var LocalPtySession = class {
625
629
  if (text.length === 0) return;
626
630
  this.lastOutputAt = Date.now();
627
631
  this.scrollback.append(text);
632
+ this.capture?.append(text);
628
633
  this.active?.append(text);
629
634
  }
630
635
  schedulePoll(operation, delayMs = this.config.pollIntervalMs) {
@@ -38,6 +38,7 @@ import { dscodeFooterHeader } from './render/status.mjs';
38
38
  import { dscodePadEnd, welcomeArtRows, welcomePath, WELCOME_ART, WELCOME_ART_SMALL } from './dscode/welcome.mjs';
39
39
  import { TETRIS_TICK_MS as DSCODE_TETRIS_TICK_MS, TETRIS_WIDTH as DSCODE_TETRIS_WIDTH, tetrisFrame as dscodeTetrisFrame } from '../../../plugins/compaction/tetris.mjs';
40
40
  import { PROVIDERS as DSCODE_PROVIDERS, providerSpec as dscodeProviderSpec, providerArgument as dscodeProviderArgument, providerOfLabel as dscodeProviderOfLabel, pickModel as dscodePickModel, credentialState as dscodeCredentialState, waitForModels as dscodeWaitForModels, } from '../../../plugins/providers/catalog.mjs';
41
+ import { grokStatusText } from '../../../plugins/grok/status.mjs';
41
42
  import { ENV_ASSIGNMENT, hasWrappingQuotes } from './provider-settings.mjs';
42
43
  import { collapseLargePaste, expandLargePastes, pasteAtomicEdit, pasteCursorEdge } from './dscode/paste.mjs';
43
44
  import { loadFlag, saveFlag } from './dscode/flags.mjs';
@@ -2228,7 +2229,32 @@ export function DscodeLoginPanel({ provider, load, save, done, back }) {
2228
2229
  });
2229
2230
  return createElement(Box, { flexDirection: "column", paddingX: 2 }, createElement(Text, { bold: true }, spec.name + " login"), createElement(Text, { dimColor: true }, "Saved on this Mac in ~/.dscode/credentials.yaml"), createElement(Text, null, busy ? "Saving…" : target ? "API key › " + (draft ? "••••••••" : "paste your key") : error ? "" : "Loading…"), error ? createElement(Text, { color: "red" }, error) : void 0, createElement(Text, { dimColor: true }, "Enter save · Esc cancel · Ctrl+U clear"));
2230
2231
  }
2231
- export function DscodeProviderPanel({ current, load, choose, back }) {
2232
+ /**
2233
+ * dscode: the Grok rail has no key to paste — its token is the local `grok login`. This panel
2234
+ * reports that login and the weekly credit window, and names the command that fixes a missing
2235
+ * or expired one. `r` re-reads both, so a login finished in another terminal shows up here.
2236
+ */
2237
+ export function DscodeGrokPanel({ snapshot, refresh, back }) {
2238
+ useStableInput((input, key) => {
2239
+ if (key.escape || key.ctrl && input === "c" || input === "q") {
2240
+ back();
2241
+ return;
2242
+ }
2243
+ if (input === "r")
2244
+ refresh();
2245
+ });
2246
+ const { status, subscription } = snapshot;
2247
+ const used = subscription?.usedPercent;
2248
+ const detail = status.kind === "ready"
2249
+ ? "Local grok login detected" + (status.expiresIn === undefined ? "" : " · token expires in about " + status.expiresIn + " min")
2250
+ : status.kind === "expired" ? "The local grok login expired."
2251
+ : status.kind === "missing" ? "No local grok login found."
2252
+ : "The local grok login file is not readable.";
2253
+ return createElement(Box, { flexDirection: "column", paddingX: 2 }, createElement(Text, { bold: true }, "Grok subscription"), createElement(Text, null, detail), ...status.kind === "ready" ? [] : [createElement(Text, { key: "fix" }, "Run grok login in a terminal, then press r to re-check.")], createElement(Text, { key: "tier" }, "Plan: " + (subscription?.tier ?? "not reported")), createElement(Text, { key: "usage" }, used === undefined
2254
+ ? "Weekly usage: the server reports none for this period"
2255
+ : "Weekly usage: " + (Number.isInteger(used) ? String(used) : used.toFixed(1)) + "% used"), ...subscription?.periodEnd === undefined ? [] : [createElement(Text, { key: "reset" }, "Resets: " + new Date(subscription.periodEnd).toLocaleString())], createElement(Text, { dimColor: true }, "r re-check · Esc close"));
2256
+ }
2257
+ export function DscodeProviderPanel({ current, load, choose, back, grokStatus }) {
2232
2258
  const [directory, setDirectory] = useState(void 0);
2233
2259
  const [failed, setFailed] = useState(false);
2234
2260
  const [cursor, setCursor] = useState(() => Math.max(0, DSCODE_PROVIDERS.findIndex(provider => provider.id === current)));
@@ -2259,6 +2285,8 @@ export function DscodeProviderPanel({ current, load, choose, back }) {
2259
2285
  }
2260
2286
  });
2261
2287
  const status = provider => {
2288
+ if (provider.id === "grok")
2289
+ return grokStatus?.() ?? "grok login not detected";
2262
2290
  if (failed)
2263
2291
  return "status unavailable";
2264
2292
  if (directory === void 0)
@@ -5306,7 +5334,9 @@ export function App(props) {
5306
5334
  return;
5307
5335
  const controller = new AbortController();
5308
5336
  const timer = setTimeout(() => controller.abort(), 8000);
5309
- fetch(DSCODE_REGISTRY_URL, { headers: { accept: 'application/vnd.npm.install-v1+json' }, signal: controller.signal })
5337
+ // Plain JSON: the registry answers 406 for the abbreviated metadata type on the
5338
+ // `/latest` dist-tag endpoint, which silently suppressed this notice.
5339
+ fetch(DSCODE_REGISTRY_URL, { headers: { accept: 'application/json' }, signal: controller.signal })
5310
5340
  .then(response => (response.ok ? response.json() : undefined))
5311
5341
  .then(data => {
5312
5342
  const latest = data?.version;
@@ -5864,7 +5894,7 @@ export function App(props) {
5864
5894
  if (state === 'missing') {
5865
5895
  setProviderOpen(false);
5866
5896
  setEffortFor(undefined);
5867
- setProviderAction({ kind: 'dscode-key', provider, then: () => dscodeSwitchProvider(provider) });
5897
+ setProviderAction(provider === 'grok' ? { kind: 'dscode-grok' } : { kind: 'dscode-key', provider, then: () => dscodeSwitchProvider(provider) });
5868
5898
  setModelOpen(true);
5869
5899
  return;
5870
5900
  }
@@ -5969,6 +5999,14 @@ export function App(props) {
5969
5999
  current: dscodeProviderOfLabel(modelLabel),
5970
6000
  load: props.loadModelProviders,
5971
6001
  choose: dscodeSwitchProvider,
6002
+ grokStatus: () => grokStatusText(props.dscodeGrokStatus?.() ?? { status: { kind: "missing" } }),
6003
+ back: closeModelSurface,
6004
+ });
6005
+ }
6006
+ else if (providerAction?.kind === 'dscode-grok') {
6007
+ modelSurface = createElement(DscodeGrokPanel, {
6008
+ snapshot: props.dscodeGrokStatus?.() ?? { status: { kind: "missing" } },
6009
+ refresh: reloadModelSurfaces,
5972
6010
  back: closeModelSurface,
5973
6011
  });
5974
6012
  }
@@ -6477,7 +6515,8 @@ export function App(props) {
6477
6515
  openLogin: (provider) => {
6478
6516
  setProviderOpen(false);
6479
6517
  setEffortFor(undefined);
6480
- setProviderAction({ kind: 'dscode-key', provider: provider ?? dscodeProviderOfLabel(modelLabel) });
6518
+ const target = provider ?? dscodeProviderOfLabel(modelLabel);
6519
+ setProviderAction(target === 'grok' ? { kind: 'dscode-grok' } : { kind: 'dscode-key', provider: target });
6481
6520
  setModelOpen(true);
6482
6521
  },
6483
6522
  openOpenRouter: () => {
@@ -19,6 +19,20 @@ export function userBackgroundRows(rows, columns, measure) {
19
19
  };
20
20
  });
21
21
  }
22
+ /** Local wall-clock HH:MM:SS for a row, or undefined when it carries no time. */
23
+ function timeStamp(time) {
24
+ if (time === undefined)
25
+ return undefined;
26
+ const at = new Date(time);
27
+ const pad = (value) => String(value).padStart(2, '0');
28
+ return pad(at.getHours()) + ':' + pad(at.getMinutes()) + ':' + pad(at.getSeconds());
29
+ }
30
+ /** Put a dim timestamp in front of a block first row. */
31
+ function stamped(lines, label) {
32
+ if (label === undefined || lines.length === 0)
33
+ return lines;
34
+ return [{ ...lines[0], segments: [lineSegment(label + ' ', 'dim'), ...lines[0].segments] }, ...lines.slice(1)];
35
+ }
22
36
  /** The folded thinking tail: one dim paragraph, capped, with the Ctrl+O hint. */
23
37
  function thinkingLines(reasoning, width) {
24
38
  const lines = hangingTextLines('Thinking: ' + reasoning.replace(/\s+/g, ' ').trim(), width, '· ', 'dimItalic', ' ');
@@ -34,15 +48,21 @@ function thinkingLines(reasoning, width) {
34
48
  */
35
49
  export function dscodeChatLines(entry, columns, verbose = false) {
36
50
  const width = Math.max(1, Math.floor(columns));
51
+ const label = entry.kind === 'assistant' || entry.kind === 'tool' ? timeStamp(entry.time) : undefined;
52
+ // The stamp leads the block, so the rows wrap to what is left of the width: a row that
53
+ // overflowed here would cost an extra screen row per block and move the composer.
54
+ const stampWidth = label === undefined ? 0 : visibleColumns(label + ' ');
55
+ const blockWidth = Math.max(1, width - stampWidth);
56
+ const blockColumns = Math.max(1, columns - stampWidth);
37
57
  if (entry.kind === 'tool') {
38
58
  if (!verbose)
39
59
  return [];
40
60
  const state = entry.state === 'running' ? ' · running' : entry.state === 'error' ? ' · error' : '';
41
- const lines = hangingStyledLines([
42
- lineSegment('Tool Call: ' + entry.name, 'dim'),
43
- lineSegment(entry.preview ? ' ' + entry.preview : '', 'dim'),
44
- lineSegment(state, entry.state === 'error' ? 'error' : 'dim'),
45
- ], width, '· ', 'dim', ' ', 'dim');
61
+ const lines = [...stamped(hangingStyledLines([
62
+ lineSegment('Tool Call: ' + entry.name, 'dim'),
63
+ lineSegment(entry.preview ? ' ' + entry.preview : '', 'dim'),
64
+ lineSegment(state, entry.state === 'error' ? 'error' : 'dim'),
65
+ ], blockWidth, '· ', 'dim', ' ', 'dim'), label)];
46
66
  if (entry.summary) {
47
67
  lines.push(...hangingTextLines('Output: ' + entry.summary, width, ' ', entry.state === 'error' ? 'error' : 'dim', ' '));
48
68
  }
@@ -50,10 +70,10 @@ export function dscodeChatLines(entry, columns, verbose = false) {
50
70
  return lines;
51
71
  }
52
72
  if (entry.kind === 'assistant') {
53
- const thinking = verbose && entry.reasoning ? [...thinkingLines(entry.reasoning, width), { segments: [] }] : [];
73
+ const thinking = verbose && entry.reasoning ? [...stamped(thinkingLines(entry.reasoning, blockWidth), label), { segments: [] }] : [];
54
74
  if (!entry.text && !entry.interrupted)
55
75
  return thinking;
56
- const body = [...thinking, ...transcriptEntryLines({ ...entry, reasoning: '' }, columns, false, false, false)];
76
+ const body = [...thinking, ...stamped(transcriptEntryLines({ ...entry, reasoning: '' }, blockColumns, false, false, false), label)];
57
77
  // The turn's last reply closes the turn with its own rule and breathing rows.
58
78
  return entry.turnEnded === true
59
79
  ? [...body, { segments: [] }, { segments: [{ text: '─'.repeat(width), style: 'dim' }] }, { segments: [] }]
@@ -25,6 +25,7 @@ import { isSlashLine, submissionPayload, watchCommands } from './commands.mjs';
25
25
  import { internals } from './internals.mjs';
26
26
  import { syncModelCapabilities } from './model-capabilities.mjs';
27
27
  import { ensureProviderRoute as dscodeEnsureProviderRoute, migrateOpenRouterProfile as dscodeMigrateOpenRouter } from '../../../plugins/providers/catalog.mjs';
28
+ import { grokStatusSnapshot } from '../../../plugins/grok/status.mjs';
28
29
  import { compactionPreview as dscodeCompactionPreview, pricedThresholdRatio as dscodePricedThresholdRatio } from '../../../plugins/compaction/threshold.mjs';
29
30
  import { dscodeLoadOpenRouterAccountFor, dscodeManagementKeyStatus, dscodeSaveManagementKey } from './app.mjs';
30
31
  import { buildModelSelection, applyModelSelectionToConfig, loadModelDirectory, modelSelectionLabel, pendingModelSelection, resolveEffectiveSelection } from './models.mjs';
@@ -2026,6 +2027,7 @@ async function run(ctx, startup, io) {
2026
2027
  quit,
2027
2028
  loadModels: () => dscodeMigrateOpenRouter(ctx.get('settings')).then(() => loadModelDirectory(ctx)),
2028
2029
  dscodeEnsureProviderRoute: (provider) => dscodeEnsureProviderRoute(ctx.get('settings'), provider),
2030
+ dscodeGrokStatus: () => grokStatusSnapshot(),
2029
2031
  dscodeManagementKeyStatus: () => dscodeManagementKeyStatus(ctx),
2030
2032
  dscodeSaveManagementKey: (key) => dscodeSaveManagementKey(ctx, key),
2031
2033
  dscodeLoadOpenRouterAccount: () => dscodeLoadOpenRouterAccountFor(ctx),
@@ -551,7 +551,7 @@ export function projectEvent(view, event) {
551
551
  ...view,
552
552
  streaming: '',
553
553
  streamingReasoning: '',
554
- ...(renderable ? { entries: [...view.entries, { kind: 'assistant', text, reasoning, interrupted: event.data.interrupted === true ? true : undefined }] } : {}),
554
+ ...(renderable ? { entries: [...view.entries, { kind: 'assistant', text, reasoning, time: event.time, interrupted: event.data.interrupted === true ? true : undefined }] } : {}),
555
555
  stats: {
556
556
  ...stats,
557
557
  llmMs: stats.llmMs + (started === undefined ? 0 : Math.max(0, event.time - started)),
@@ -591,6 +591,7 @@ export function projectEvent(view, event) {
591
591
  ...view.entries,
592
592
  {
593
593
  kind: 'tool',
594
+ time: event.time,
594
595
  callId: data.callId,
595
596
  ordinal,
596
597
  name: data.name,
@@ -1314,7 +1315,7 @@ export function replayProjectEvent(acc, event) {
1314
1315
  // Same zero-line guard as the live fold: tool-only settlements carry
1315
1316
  // timing/usage but no renderable transcript entry.
1316
1317
  if (text !== '' || reasoning !== '' || event.data.interrupted === true) {
1317
- appendReplayEntry(acc, { kind: 'assistant', text, reasoning, interrupted: event.data.interrupted === true ? true : undefined });
1318
+ appendReplayEntry(acc, { kind: 'assistant', text, reasoning, time: event.time, interrupted: event.data.interrupted === true ? true : undefined });
1318
1319
  }
1319
1320
  acc.stats = {
1320
1321
  ...acc.stats,
@@ -1346,6 +1347,7 @@ export function replayProjectEvent(acc, event) {
1346
1347
  acc.toolCallOrdinal += 1;
1347
1348
  appendReplayEntry(acc, {
1348
1349
  kind: 'tool',
1350
+ time: event.time,
1349
1351
  callId: data.callId,
1350
1352
  ordinal: acc.toolCallOrdinal,
1351
1353
  name: data.name,