@spinalfluid/ultrac 0.1.18 → 0.1.20

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.
Files changed (3) hide show
  1. package/README.md +11 -0
  2. package/dist/cli.js +155 -77
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -764,3 +764,14 @@ when a tool needs an ungranted permission.
764
764
  These presets do not bypass operating-system permissions or the restrictions of a read-only
765
765
  agent mode. Approval dialogs offer separate choices for one call, the session, denial, or
766
766
  session-long refusal, with keyboard selection in interactive terminals.
767
+
768
+ After a terminal permission selection, the approval dialog collapses to the tool, permission,
769
+ and selected choice. Activity and progress messages stay in chronological transcript order,
770
+ with spacing between groups; command details remain available through the activity dropdown.
771
+
772
+ Progress prose comes from the model and describes the actual work or verified findings. The CLI
773
+ does not generate periodic tool-count, failure-count, or model-waiting messages.
774
+
775
+ Use `/permissions` to open the interactive permission-mode picker, `/permissions status` to
776
+ inspect grants, or `/permissions readonly`, `/permissions workspace`, and `/permissions full`
777
+ to change modes directly for the current session.
package/dist/cli.js CHANGED
@@ -10834,6 +10834,8 @@ ${mode.instructions.map((instruction) => `- ${instruction}`).join("\n")}
10834
10834
  ${autonomyLines}
10835
10835
  ${UNTRUSTED_CONTENT_GUIDANCE}
10836
10836
 
10837
+ For long tasks, occasionally give a brief user-facing update about the current work or verified findings. Avoid repetitive updates, tool-call counts, failure counts, and boilerplate about waiting for the model. Do not invent findings or claim work that has not happened.
10838
+
10837
10839
  ${nativeTools ? "Write complete code files or append complete sections across tool calls for large implementations. Verify files with tests or a compiler. Never replace requested code with placeholders or ellipses. Keep final explanations concise. Use the provided native function tools for actions. Do not put a JSON tool-call protocol in your answer. When the requested work is complete, give the final answer without calling more tools." : `Respond only with a JSON object:
10838
10840
  {
10839
10841
  "message": "short user-facing status or final answer",
@@ -17005,6 +17007,8 @@ ${mode.instructions.map((instruction) => `- ${instruction}`).join("\n")}
17005
17007
 
17006
17008
  ${UNTRUSTED_CONTENT_GUIDANCE}
17007
17009
 
17010
+ For long tasks, occasionally give a brief user-facing update about the current work or verified findings. Avoid repetitive updates, tool-call counts, failure counts, and boilerplate about waiting for the model. Do not invent findings or claim work that has not happened.
17011
+
17008
17012
  For large code generation, write complete files or append complete sections across multiple tool calls. Verify the files with the project tests or compiler. Never use ellipses or placeholders for requested implementation, and do not declare success with unfinished code. Keep final explanations concise; code completeness takes priority over response brevity.
17009
17013
 
17010
17014
  Use the provided tools to inspect and change the workspace. Prefer narrow, reversible changes and
@@ -17636,6 +17640,16 @@ function formatSeconds(milliseconds) {
17636
17640
 
17637
17641
  // src/ui/activity.ts
17638
17642
  var sink;
17643
+ var messageSink;
17644
+ function setActivityMessageSink(value) {
17645
+ messageSink = value;
17646
+ }
17647
+ function writeActivityMessage(text) {
17648
+ if (messageSink) messageSink(text);
17649
+ else console.log(`
17650
+ ${text}
17651
+ `);
17652
+ }
17639
17653
  var workingSink;
17640
17654
  function setWorkingSink(value) {
17641
17655
  workingSink = value;
@@ -17670,7 +17684,7 @@ function activityRows(snapshot, expanded, selected, end) {
17670
17684
  const summary = `${expanded ? "\u25BE" : "\u25B8"} \u25CF ${snapshot.entries.length} ${snapshot.entries.length === 1 ? "tool call" : "tool calls"}${failures ? ` \xB7 ${failures} failed` : ""}${snapshot.finished ? elapsed : ""} \xB7 Ctrl+O details`;
17671
17685
  const rows = [{ text: summary, ok: failures || snapshot.failed ? false : snapshot.finished ? true : void 0 }];
17672
17686
  if (!expanded) {
17673
- if (!snapshot.finished) for (let index = Math.max(0, snapshot.entries.length - 3); index < snapshot.entries.length; index += 1) {
17687
+ if (!snapshot.finished || snapshot.preview) for (let index = Math.max(0, snapshot.entries.length - 3); index < snapshot.entries.length; index += 1) {
17674
17688
  const entry = snapshot.entries[index];
17675
17689
  rows.push({ text: ` \u25CF ${entry.label}`, ok: entry.ok, entry: index });
17676
17690
  }
@@ -17690,32 +17704,6 @@ function activityRows(snapshot, expanded, selected, end) {
17690
17704
  return rows;
17691
17705
  }
17692
17706
 
17693
- // src/ui/task-progress.ts
17694
- function startTaskProgress(options) {
17695
- let lastMessage = performance.now();
17696
- let lastState = "";
17697
- let stopped = false;
17698
- const timer = setInterval(() => {
17699
- if (stopped || options.holdWhile?.()) return;
17700
- const state = options.snapshot();
17701
- if (!state.activeTool && !state.waitingForModel) return;
17702
- const fingerprint = `${state.completed}:${state.failed}:${state.activeTool ?? "model"}`;
17703
- if (performance.now() - lastMessage < (fingerprint === lastState ? 6e4 : 3e4)) return;
17704
- const completed = state.completed ? `I have completed ${state.completed} tool ${state.completed === 1 ? "call" : "calls"}.${state.failed ? ` Of those, ${state.failed} failed.` : ""} ` : "";
17705
- const current = state.activeTool ? `I am running the ${state.activeTool} tool.` : "I am waiting for the model's next response.";
17706
- options.write(`${completed}${current}`);
17707
- lastMessage = performance.now();
17708
- lastState = fingerprint;
17709
- }, 5e3);
17710
- timer.unref?.();
17711
- return { markMessage: () => {
17712
- lastMessage = performance.now();
17713
- }, stop: () => {
17714
- stopped = true;
17715
- clearInterval(timer);
17716
- } };
17717
- }
17718
-
17719
17707
  // src/ui/interactive-choice.ts
17720
17708
  var MOUSE_ON = "\x1B[?1006h\x1B[?1000h";
17721
17709
  var MOUSE_OFF = "\x1B[?1000l\x1B[?1006l";
@@ -18117,6 +18105,9 @@ var TerminalLayout = class {
18117
18105
  prompt = "\u203A ";
18118
18106
  inputFormatter;
18119
18107
  transcriptRow = 1;
18108
+ transientBlocks = /* @__PURE__ */ new Set();
18109
+ activityOffset = 0;
18110
+ totalActivityEntries = 0;
18120
18111
  transcriptColumn = 1;
18121
18112
  wrapPending = false;
18122
18113
  savedTranscript;
@@ -18230,7 +18221,61 @@ var TerminalLayout = class {
18230
18221
  };
18231
18222
  this.paintFooter();
18232
18223
  }
18224
+ beginTransientBlock() {
18225
+ const anchor = { row: this.transcriptRow };
18226
+ this.transientBlocks.add(anchor);
18227
+ return anchor;
18228
+ }
18229
+ collapseTransientBlock(anchor, summary) {
18230
+ if (!this.transientBlocks.delete(anchor) || this.closed) return;
18231
+ const start = Math.max(1, Math.min(anchor.row, this.transcriptRow));
18232
+ const removed = Math.max(0, this.transcriptRow - start);
18233
+ if (removed) this.rawWrite(`${cup(start)}${CSI}${removed}M`);
18234
+ this.rawWrite(`${cup(start)}${CSI}2K`);
18235
+ for (const item of this.completedActivities) if (item.row >= start) item.row -= removed;
18236
+ this.completedActivities = this.completedActivities.filter((item) => item.row >= 1);
18237
+ this.transcriptRow = start;
18238
+ this.transcriptColumn = 1;
18239
+ this.wrapPending = false;
18240
+ this.writeTranscript(`${summary}
18241
+
18242
+ `);
18243
+ this.paintFooter();
18244
+ }
18245
+ writeActivityMessage(text) {
18246
+ const item = this.liveActivity;
18247
+ if (item) {
18248
+ if (item.snapshot.entries.length) {
18249
+ item.snapshot = { ...item.snapshot, finished: true, preview: true };
18250
+ if (item.row >= 2) this.rawWrite(`${cup(item.row - 1)}${CSI}2K`);
18251
+ this.redrawCompleted(item);
18252
+ } else {
18253
+ const start = Math.max(1, item.row - 1);
18254
+ const removed = item.row >= 1 ? Math.max(0, Math.min(item.height + (item.row >= 2 ? 1 : 0), this.transcriptRow - start)) : 0;
18255
+ if (removed) this.rawWrite(`${cup(start)}${CSI}${removed}M`);
18256
+ this.transcriptRow -= removed;
18257
+ this.completedActivities = this.completedActivities.filter((entry) => entry !== item);
18258
+ }
18259
+ this.liveActivity = void 0;
18260
+ }
18261
+ this.activityOffset = this.totalActivityEntries;
18262
+ this.writeTranscript(`
18263
+ ${text}
18264
+
18265
+ `);
18266
+ if (item) {
18267
+ const snapshot = { ...item.snapshot, entries: [], finished: false, preview: false };
18268
+ this.writeTranscript(`
18269
+ ${this.colorActivityRow(activityRows(snapshot, false, -1, 0)[0])}
18270
+ `);
18271
+ this.liveActivity = { snapshot, row: this.transcriptRow - 1, height: 1, expanded: false, selected: -1, end: 0 };
18272
+ this.completedActivities.push(this.liveActivity);
18273
+ }
18274
+ }
18233
18275
  setActivity(snapshot) {
18276
+ if (!this.liveActivity && snapshot.entries.length < this.totalActivityEntries) this.activityOffset = 0;
18277
+ this.totalActivityEntries = snapshot.entries.length;
18278
+ snapshot = { ...snapshot, entries: snapshot.entries.slice(this.activityOffset) };
18234
18279
  this.activity = snapshot;
18235
18280
  if (!this.enabled || !this.installed || this.closed) return;
18236
18281
  if (!this.liveActivity && !snapshot.finished) {
@@ -18257,9 +18302,13 @@ ${this.colorActivityRow(activityRows(snapshot, false, -1, snapshot.entries.lengt
18257
18302
  }
18258
18303
  this.completedActivities = this.completedActivities.filter((entry) => entry !== item);
18259
18304
  this.liveActivity = void 0;
18260
- this.writeTranscript(`${this.colorActivityRow(activityRows(snapshot, false, -1, snapshot.entries.length)[0])}
18305
+ this.activityOffset = 0;
18306
+ this.totalActivityEntries = 0;
18307
+ if (snapshot.entries.length) {
18308
+ this.writeTranscript(`${this.colorActivityRow(activityRows(snapshot, false, -1, snapshot.entries.length)[0])}
18261
18309
  `);
18262
- this.completedActivities.push({ ...item, row: this.transcriptRow - 1, height: 1, expanded: false, selected: -1 });
18310
+ this.completedActivities.push({ ...item, row: this.transcriptRow - 1, height: 1, expanded: false, selected: -1 });
18311
+ }
18263
18312
  this.paintFooter();
18264
18313
  }
18265
18314
  setWorkingStatus(text) {
@@ -18290,6 +18339,7 @@ ${this.colorActivityRow(activityRows(item.snapshot, false, -1, item.end)[0])}
18290
18339
  return dot >= 0 && color ? `${text.slice(0, dot)}${color}\u25CF${CSI}39m${text.slice(dot + 1)}` : text;
18291
18340
  }
18292
18341
  shiftCompleted(rows) {
18342
+ for (const anchor of this.transientBlocks) anchor.row -= rows;
18293
18343
  for (const item of this.completedActivities) item.row -= rows;
18294
18344
  this.completedActivities = this.completedActivities.filter((item) => item.row >= 1);
18295
18345
  }
@@ -18313,7 +18363,7 @@ ${this.colorActivityRow(activityRows(item.snapshot, false, -1, item.end)[0])}
18313
18363
  }
18314
18364
  toggleActivity() {
18315
18365
  if (this.liveActivity && (this.liveActivity.row < 2 || this.liveActivity.row + this.liveActivity.height > this.contentRows)) this.setWorkingStatus("working\u2026");
18316
- const item = this.liveActivity ?? this.completedActivities.at(-1);
18366
+ const item = this.liveActivity?.snapshot.entries.length ? this.liveActivity : [...this.completedActivities].reverse().find((entry) => entry.snapshot.entries.length > 0) ?? this.liveActivity;
18317
18367
  if (item) {
18318
18368
  item.expanded = !item.expanded;
18319
18369
  this.redrawCompleted(item);
@@ -18926,7 +18976,7 @@ var HELP = `Commands:
18926
18976
  /autonomous [on|off|status] Continue step-limited turns (up to five rounds)
18927
18977
  /model [name] Show or set the model
18928
18978
  /mode [name] Show or set the agent mode (${agentModeNames.join(", ")})
18929
- /permissions [readonly|workspace|full] Show or set permissions
18979
+ /permissions [readonly|workspace|full|status] Choose or set tool access
18930
18980
  /deny <perm> Revoke a permission for this session
18931
18981
  /plan [task] Plan without workspace edits
18932
18982
  /review [focus] Review current changes without editing
@@ -19125,6 +19175,7 @@ async function runRepl(options, initialTask = "") {
19125
19175
  }) : void 0;
19126
19176
  setActivitySink(layout?.enabled ? (snapshot) => layout.setActivity(snapshot) : void 0);
19127
19177
  setWorkingSink(layout?.enabled ? (text) => layout.setWorkingStatus(text) : void 0);
19178
+ setActivityMessageSink(layout?.enabled ? (text) => layout.writeActivityMessage(text) : void 0);
19128
19179
  const refreshDetails = () => {
19129
19180
  if (!layout?.enabled) return;
19130
19181
  const steps = session.toSession().steps;
@@ -19209,36 +19260,44 @@ async function runRepl(options, initialTask = "") {
19209
19260
  };
19210
19261
  const drainQueuedMessages = () => reader.drainQueuedMessages();
19211
19262
  let autonomousRounds = 5;
19212
- setAskUserRuntime(async (text, options2, signal) => {
19213
- console.log(chalk11.white(`
19263
+ setAskUserRuntime(async (text, options2, signal, presentation) => {
19264
+ const block = presentation && layout?.enabled ? layout.beginTransientBlock() : void 0;
19265
+ let resolvedAnswer = null;
19266
+ try {
19267
+ console.log(chalk11.white(`
19214
19268
  ? ${text}`));
19215
- let rawAnswer;
19216
- if (options2?.length && layout?.enabled) {
19217
- slashCompletion?.suspend();
19218
- layout.setPrompt("\u203A ");
19219
- let choices;
19220
- try {
19221
- choices = attachInteractiveChoices(rl, options2, { signal, output: stdout5, prompt: "\u203A ", maxRow: () => layout.contentRows });
19222
- rawAnswer = await reader.nextFresh(signal);
19223
- } finally {
19224
- choices?.cleanup();
19269
+ let rawAnswer;
19270
+ if (options2?.length && layout?.enabled) {
19271
+ slashCompletion?.suspend();
19225
19272
  layout.setPrompt("\u203A ");
19226
- slashCompletion?.resume();
19273
+ let choices;
19274
+ try {
19275
+ choices = attachInteractiveChoices(rl, options2, { signal, output: stdout5, prompt: "\u203A ", maxRow: () => layout.contentRows });
19276
+ rawAnswer = await reader.nextFresh(signal);
19277
+ } finally {
19278
+ choices?.cleanup();
19279
+ layout.setPrompt("\u203A ");
19280
+ slashCompletion?.resume();
19281
+ }
19282
+ } else {
19283
+ if (options2?.length) {
19284
+ options2.forEach((option, index) => console.log(chalk11.gray(` ${index + 1}. ${option}`)));
19285
+ console.log(chalk11.gray(" (type a number, or answer in your own words)"));
19286
+ }
19287
+ rawAnswer = await question(chalk11.white(" \u203A "), signal);
19227
19288
  }
19228
- } else {
19229
- if (options2?.length) {
19230
- options2.forEach((option, index) => console.log(chalk11.gray(` ${index + 1}. ${option}`)));
19231
- console.log(chalk11.gray(" (type a number, or answer in your own words)"));
19289
+ if (signal?.aborted) return null;
19290
+ const answer = (rawAnswer ?? "").trim();
19291
+ const picked = Number.parseInt(answer, 10);
19292
+ if (options2?.length && String(picked) === answer && picked >= 1 && picked <= options2.length) {
19293
+ resolvedAnswer = options2[picked - 1];
19294
+ return resolvedAnswer;
19232
19295
  }
19233
- rawAnswer = await question(chalk11.white(" \u203A "), signal);
19234
- }
19235
- if (signal?.aborted) return null;
19236
- const answer = (rawAnswer ?? "").trim();
19237
- const picked = Number.parseInt(answer, 10);
19238
- if (options2?.length && String(picked) === answer && picked >= 1 && picked <= options2.length) {
19239
- return options2[picked - 1];
19296
+ resolvedAnswer = rawAnswer === null ? null : answer;
19297
+ return resolvedAnswer;
19298
+ } finally {
19299
+ if (block && presentation) layout?.collapseTransientBlock(block, presentation.collapseSummary(signal?.aborted ? null : resolvedAnswer));
19240
19300
  }
19241
- return answer;
19242
19301
  });
19243
19302
  let running = true;
19244
19303
  let turnAbort;
@@ -19366,6 +19425,7 @@ async function runRepl(options, initialTask = "") {
19366
19425
  setAskUserRuntime(void 0);
19367
19426
  setActivitySink(void 0);
19368
19427
  setWorkingSink(void 0);
19428
+ setActivityMessageSink(void 0);
19369
19429
  rl.close();
19370
19430
  try {
19371
19431
  await saveTranscript(session, writeCheckpoint);
@@ -19405,17 +19465,6 @@ async function runTurn(session, input, question, signal, drainQueuedMessages, pe
19405
19465
  const detachProgress = quiet ? () => {
19406
19466
  } : attachProgressUi();
19407
19467
  let deltaChars = 0;
19408
- const progress2 = quiet ? void 0 : startTaskProgress({
19409
- snapshot: () => ({
19410
- completed: entries.length,
19411
- failed: entries.filter((entry) => !entry.ok).length,
19412
- activeTool: [...activeTools.keys()].at(-1)?.tool,
19413
- waitingForModel: thinkingStarted !== void 0
19414
- }),
19415
- holdWhile: () => permissionPrompts > 0 || [...activeTools.keys()].some((call) => call.tool === "ask_user"),
19416
- write: (text) => console.log(`
19417
- ${text}`)
19418
- });
19419
19468
  try {
19420
19469
  const prepared = allowImageDrop ? await prepareImageInput(input) : { text: input };
19421
19470
  await session.ask(prepared.text, {
@@ -19450,9 +19499,7 @@ ${text}`)
19450
19499
  },
19451
19500
  onAssistantMessage: (message) => {
19452
19501
  if (message.trim()) {
19453
- progress2?.markMessage();
19454
- console.log(`
19455
- ${message.trim()}`);
19502
+ writeActivityMessage(message.trim());
19456
19503
  }
19457
19504
  },
19458
19505
  onToolStart: (call) => {
@@ -19513,7 +19560,11 @@ ${message.trim()}`);
19513
19560
  Answer: `;
19514
19561
  const choices = ["Allow once", "Allow for this session", "Deny this call", "Never allow this permission this session"];
19515
19562
  const asker = getAskUserRuntime();
19516
- const rawAnswer = asker ? await asker(prompt.split("\n").slice(0, -5).join("\n"), choices, signal) : await question(chalk11.yellow(prompt), signal);
19563
+ const rawAnswer = asker ? await asker(prompt.split("\n").slice(0, -5).join("\n"), choices, signal, { collapseSummary: (value) => {
19564
+ const answer2 = value?.trim().toLowerCase();
19565
+ const label = !answer2 ? "Cancelled" : ["a", "always", "2", choices[1].toLowerCase()].includes(answer2) ? choices[1] : ["o", "once", "y", "yes", "1", choices[0].toLowerCase()].includes(answer2) ? choices[0] : ["e", "never", "4", choices[3].toLowerCase()].includes(answer2) ? choices[3] : choices[2];
19566
+ return ` ${call.tool} \xB7 ${permission} permission: ${label}`;
19567
+ } }) : await question(chalk11.yellow(prompt), signal);
19517
19568
  if (signal?.aborted || rawAnswer === null) return "deny";
19518
19569
  const answer = rawAnswer.trim().toLowerCase();
19519
19570
  if (answer === "a" || answer === "always" || answer === "2" || answer === choices[1].toLowerCase()) return "always";
@@ -19545,7 +19596,6 @@ ${message.trim()}`);
19545
19596
  activeTools.clear();
19546
19597
  if (thinkingStarted !== void 0) thinkingMs += performance.now() - thinkingStarted;
19547
19598
  refreshActivity(true);
19548
- progress2?.stop();
19549
19599
  detachProgress();
19550
19600
  }
19551
19601
  }
@@ -19823,15 +19873,43 @@ ${extra}`);
19823
19873
  return "ok";
19824
19874
  }
19825
19875
  case "permissions": {
19826
- if (argument) {
19827
- if (!["readonly", "workspace", "full"].includes(argument)) {
19828
- console.log("Usage: /permissions [readonly|workspace|full]");
19876
+ let selectedMode = argument.toLowerCase();
19877
+ const modes = ["readonly", "workspace", "full"];
19878
+ const choices = ["Readonly: no write, shell, or network access", "Workspace: file writes; shell and network denied", "Full access: no tool approval prompts"];
19879
+ const resolveMode = (value) => {
19880
+ if (value === null) return void 0;
19881
+ const text = value.trim().toLowerCase();
19882
+ const index = choices.findIndex((choice) => choice.toLowerCase() === text);
19883
+ if (index >= 0) return modes[index];
19884
+ if (["1", "2", "3"].includes(text)) return modes[Number(text) - 1];
19885
+ return modes.includes(text) ? text : void 0;
19886
+ };
19887
+ const asker = getAskUserRuntime();
19888
+ if (!selectedMode && asker) {
19889
+ const answer = await asker("Choose tool permissions for this session.", choices, signal, {
19890
+ collapseSummary: (value) => {
19891
+ const mode2 = resolveMode(value);
19892
+ return mode2 ? ` Permission mode: ${mode2}` : " Permission mode unchanged.";
19893
+ }
19894
+ });
19895
+ if (signal?.aborted || answer === null) return "ok";
19896
+ const mode = resolveMode(answer);
19897
+ if (!mode) {
19898
+ console.log("Permission mode unchanged. Use /permissions readonly|workspace|full.");
19899
+ return "ok";
19900
+ }
19901
+ selectedMode = mode;
19902
+ }
19903
+ if (selectedMode && selectedMode !== "status") {
19904
+ if (!modes.includes(selectedMode)) {
19905
+ console.log("Usage: /permissions [readonly|workspace|full|status]");
19829
19906
  return "ok";
19830
19907
  }
19831
19908
  for (const permission of ["write", "shell", "network"]) {
19832
- session.setPermission(permission, argument === "full" || argument === "workspace" && permission === "write");
19909
+ session.setPermission(permission, selectedMode === "full" || selectedMode === "workspace" && permission === "write");
19833
19910
  }
19834
- console.log(argument === "full" ? "Full access: file, shell, and network tools run without approval prompts for this session." : "Permissions updated. Ungranted tools are denied without prompting; /allow restores access.");
19911
+ if (argument) console.log(selectedMode === "full" ? "Full access: file, shell, and network tools run without approval prompts for this session." : `Permission mode: ${selectedMode}. Ungranted tools are denied without prompting.`);
19912
+ return "ok";
19835
19913
  }
19836
19914
  const p = session.currentPermissions;
19837
19915
  console.log(`write: ${p.write} shell: ${p.shell} network: ${p.network}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spinalfluid/ultrac",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "Ultrac terminal agent harness for Cosmic 1, with local tools, browser sign-in, streamed answers, and resumable sessions.",
6
6
  "author": "Aditya Pundir",