@oh-my-pi/pi-coding-agent 15.9.0 → 15.9.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.
Files changed (124) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/types/cli/dry-balance-cli.d.ts +104 -0
  3. package/dist/types/cli/update-cli.d.ts +15 -1
  4. package/dist/types/commands/dry-balance.d.ts +31 -0
  5. package/dist/types/config/append-only-context-mode.d.ts +8 -0
  6. package/dist/types/config/model-registry.d.ts +5 -0
  7. package/dist/types/config/models-config-schema.d.ts +18 -0
  8. package/dist/types/config/settings-schema.d.ts +3 -3
  9. package/dist/types/config/settings.d.ts +11 -0
  10. package/dist/types/discovery/helpers.d.ts +1 -0
  11. package/dist/types/exa/mcp-client.d.ts +2 -1
  12. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -3
  13. package/dist/types/hindsight/bank.d.ts +17 -9
  14. package/dist/types/hindsight/mental-models.d.ts +1 -1
  15. package/dist/types/hindsight/state.d.ts +9 -3
  16. package/dist/types/mcp/json-rpc.d.ts +6 -1
  17. package/dist/types/mcp/manager.d.ts +1 -1
  18. package/dist/types/mcp/tool-bridge.d.ts +4 -0
  19. package/dist/types/mnemopi/state.d.ts +2 -2
  20. package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
  21. package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -0
  22. package/dist/types/modes/components/plugin-settings.d.ts +40 -8
  23. package/dist/types/modes/components/session-selector.d.ts +8 -3
  24. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  25. package/dist/types/modes/components/transcript-container.d.ts +3 -2
  26. package/dist/types/modes/utils/keybinding-matchers.d.ts +4 -0
  27. package/dist/types/session/agent-session.d.ts +13 -1
  28. package/dist/types/session/auth-storage.d.ts +2 -2
  29. package/dist/types/session/history-storage.d.ts +3 -4
  30. package/dist/types/session/messages.d.ts +1 -0
  31. package/dist/types/session/session-manager.d.ts +1 -0
  32. package/dist/types/slash-commands/types.d.ts +17 -4
  33. package/dist/types/task/types.d.ts +2 -0
  34. package/dist/types/tiny/text.d.ts +17 -0
  35. package/dist/types/tools/index.d.ts +16 -0
  36. package/dist/types/tools/path-utils.d.ts +11 -0
  37. package/dist/types/web/search/providers/base.d.ts +14 -0
  38. package/dist/types/web/search/providers/exa.d.ts +9 -0
  39. package/dist/types/web/search/providers/perplexity.d.ts +2 -2
  40. package/dist/types/web/search/types.d.ts +2 -1
  41. package/package.json +9 -9
  42. package/src/cli/dry-balance-cli.ts +823 -0
  43. package/src/cli/session-picker.ts +1 -0
  44. package/src/cli/update-cli.ts +54 -2
  45. package/src/cli-commands.ts +1 -0
  46. package/src/commands/completions.ts +1 -1
  47. package/src/commands/dry-balance.ts +43 -0
  48. package/src/config/append-only-context-mode.ts +37 -0
  49. package/src/config/model-registry.ts +6 -0
  50. package/src/config/models-config-schema.ts +3 -0
  51. package/src/config/settings-schema.ts +2 -2
  52. package/src/config/settings.ts +38 -0
  53. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -0
  54. package/src/discovery/github.ts +37 -1
  55. package/src/discovery/helpers.ts +3 -1
  56. package/src/exa/mcp-client.ts +11 -5
  57. package/src/extensibility/plugins/legacy-pi-compat.ts +245 -25
  58. package/src/hindsight/backend.ts +184 -35
  59. package/src/hindsight/bank.ts +32 -22
  60. package/src/hindsight/mental-models.ts +1 -1
  61. package/src/hindsight/state.ts +21 -7
  62. package/src/internal-urls/docs-index.generated.ts +5 -5
  63. package/src/internal-urls/omp-protocol.ts +8 -2
  64. package/src/main.ts +4 -2
  65. package/src/mcp/json-rpc.ts +8 -0
  66. package/src/mcp/manager.ts +40 -21
  67. package/src/mcp/render.ts +3 -0
  68. package/src/mcp/tool-bridge.ts +10 -2
  69. package/src/mcp/transports/http.ts +33 -16
  70. package/src/mnemopi/state.ts +4 -4
  71. package/src/modes/acp/acp-agent.ts +168 -3
  72. package/src/modes/components/agent-dashboard.ts +103 -31
  73. package/src/modes/components/extensions/extension-dashboard.ts +56 -10
  74. package/src/modes/components/history-search.ts +128 -14
  75. package/src/modes/components/plugin-settings.ts +270 -36
  76. package/src/modes/components/session-selector.ts +45 -14
  77. package/src/modes/components/settings-selector.ts +1 -1
  78. package/src/modes/components/tips.txt +5 -1
  79. package/src/modes/components/transcript-container.ts +35 -6
  80. package/src/modes/components/tree-selector.ts +29 -2
  81. package/src/modes/controllers/command-controller.ts +4 -3
  82. package/src/modes/controllers/input-controller.ts +18 -7
  83. package/src/modes/controllers/selector-controller.ts +30 -19
  84. package/src/modes/interactive-mode.ts +38 -3
  85. package/src/modes/setup-wizard/scenes/sign-in.ts +27 -7
  86. package/src/modes/utils/keybinding-matchers.ts +10 -0
  87. package/src/prompts/agents/explore.md +1 -0
  88. package/src/prompts/agents/librarian.md +1 -0
  89. package/src/prompts/dry-balance-bench.md +8 -0
  90. package/src/prompts/steering/user-interjection.md +10 -0
  91. package/src/prompts/system/agent-creation-architect.md +1 -26
  92. package/src/prompts/system/system-prompt.md +143 -145
  93. package/src/prompts/system/title-system.md +3 -2
  94. package/src/prompts/tools/browser.md +29 -29
  95. package/src/prompts/tools/render-mermaid.md +2 -2
  96. package/src/sdk.ts +87 -30
  97. package/src/session/agent-session.ts +96 -14
  98. package/src/session/auth-storage.ts +4 -0
  99. package/src/session/history-storage.ts +11 -18
  100. package/src/session/messages.ts +80 -0
  101. package/src/session/session-manager.ts +7 -1
  102. package/src/slash-commands/types.ts +27 -10
  103. package/src/task/executor.ts +6 -2
  104. package/src/task/index.ts +8 -7
  105. package/src/task/types.ts +2 -0
  106. package/src/tiny/text.ts +112 -1
  107. package/src/tools/bash.ts +3 -4
  108. package/src/tools/index.ts +16 -0
  109. package/src/tools/job.ts +3 -3
  110. package/src/tools/memory-recall.ts +1 -1
  111. package/src/tools/memory-reflect.ts +3 -3
  112. package/src/tools/path-utils.ts +21 -0
  113. package/src/tools/search.ts +18 -1
  114. package/src/tools/ssh.ts +26 -10
  115. package/src/tools/write.ts +14 -2
  116. package/src/tui/status-line.ts +15 -4
  117. package/src/utils/file-mentions.ts +7 -107
  118. package/src/utils/title-generator.ts +66 -38
  119. package/src/web/search/index.ts +3 -1
  120. package/src/web/search/provider.ts +1 -1
  121. package/src/web/search/providers/base.ts +17 -0
  122. package/src/web/search/providers/exa.ts +111 -7
  123. package/src/web/search/providers/perplexity.ts +8 -4
  124. package/src/web/search/types.ts +2 -1
@@ -441,8 +441,35 @@ class TreeList implements Component {
441
441
  const lines: string[] = [];
442
442
 
443
443
  if (this.#filteredNodes.length === 0) {
444
- lines.push(truncateToWidth(theme.fg("muted", " No entries found"), width));
445
- lines.push(truncateToWidth(theme.fg("muted", ` (0/0)${this.#getFilterLabel()}`), width));
444
+ // Three empty-state shapes:
445
+ // - flatNodes empty → no entries at all (truly fresh session).
446
+ // - search query rejects everything → tell the user the search is the cause.
447
+ // - filter mode rejects everything → tell the user the filter is the cause and
448
+ // how to widen it. Otherwise fresh sessions whose only persisted entries are
449
+ // `model_change` + `thinking_level_change` (both hidden by the default filter)
450
+ // read as "broken /tree" — see #1909.
451
+ if (this.#flatNodes.length === 0) {
452
+ lines.push(truncateToWidth(theme.fg("muted", " No entries found"), width));
453
+ lines.push(truncateToWidth(theme.fg("muted", ` (0/0)${this.#getFilterLabel()}`), width));
454
+ } else if (this.#searchQuery.length > 0) {
455
+ lines.push(truncateToWidth(theme.fg("muted", ` No entries match search "${this.#searchQuery}"`), width));
456
+ lines.push(truncateToWidth(theme.fg("muted", " Press Backspace to clear the search"), width));
457
+ lines.push(
458
+ truncateToWidth(theme.fg("muted", ` (0/${this.#flatNodes.length})${this.#getFilterLabel()}`), width),
459
+ );
460
+ } else {
461
+ const filterLabel = this.#getFilterLabel().trim() || "[default]";
462
+ lines.push(
463
+ truncateToWidth(
464
+ theme.fg("muted", ` ${this.#flatNodes.length} entries hidden by the current filter ${filterLabel}`),
465
+ width,
466
+ ),
467
+ );
468
+ lines.push(truncateToWidth(theme.fg("muted", " Press Alt+A to show all, Alt+D for default"), width));
469
+ lines.push(
470
+ truncateToWidth(theme.fg("muted", ` (0/${this.#flatNodes.length})${this.#getFilterLabel()}`), width),
471
+ );
472
+ }
446
473
  return lines;
447
474
  }
448
475
 
@@ -13,6 +13,7 @@ import {
13
13
  import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui";
14
14
  import { formatDuration, Snowflake } from "@oh-my-pi/pi-utils";
15
15
  import { $ } from "bun";
16
+ import { shouldEnableAppendOnlyContext } from "../../config/append-only-context-mode";
16
17
  import { loadCustomShare } from "../../export/custom-share";
17
18
  import type { CompactOptions } from "../../extensibility/extensions/types";
18
19
  import {
@@ -397,10 +398,10 @@ export class CommandController {
397
398
  // Append-only context
398
399
  {
399
400
  const setting = this.ctx.settings.get("provider.appendOnlyContext") ?? "auto";
400
- const provider = this.ctx.session.model?.provider;
401
- const mode = setting === "on" ? true : setting === "off" ? false : provider === "deepseek";
401
+ const model = this.ctx.session.model;
402
+ const mode = shouldEnableAppendOnlyContext(setting, model);
402
403
  const activeLabel = mode ? theme.fg("success", "active") : theme.fg("dim", "inactive");
403
- const settingLabel = setting === "auto" ? `${setting} (${provider ?? "?"})` : setting;
404
+ const settingLabel = setting === "auto" ? `${setting} (${model?.provider ?? "?"})` : setting;
404
405
  info += `${theme.fg("dim", "Append-Only:")} ${activeLabel} (setting: ${settingLabel})\n`;
405
406
  }
406
407
  info += `${theme.bold("Tokens")}\n`;
@@ -1,7 +1,6 @@
1
1
  import * as fs from "node:fs/promises";
2
- import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
3
2
  import type { AutocompleteProvider, SlashCommand } from "@oh-my-pi/pi-tui";
4
- import { $env, sanitizeText } from "@oh-my-pi/pi-utils";
3
+ import { $env, logger, sanitizeText } from "@oh-my-pi/pi-utils";
5
4
  import { getRoleInfo } from "../../config/model-registry";
6
5
  import { isSettingsInitialized, settings } from "../../config/settings";
7
6
  import { renderSegmentTrack } from "../../modes/components/segment-track";
@@ -13,6 +12,7 @@ import type { AgentSessionEvent } from "../../session/agent-session";
13
12
  import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
14
13
  import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
15
14
  import { isTinyTitleLocalModelKey } from "../../tiny/models";
15
+ import { isLowSignalTitleInput } from "../../tiny/text";
16
16
  import { tinyTitleClient } from "../../tiny/title-client";
17
17
  import type { TinyTitleProgressEvent } from "../../tiny/title-protocol";
18
18
  import { copyToClipboard, readImageFromClipboard, readTextFromClipboard } from "../../utils/clipboard";
@@ -376,9 +376,12 @@ export class InputController {
376
376
  // First, move any pending bash components to chat
377
377
  this.ctx.flushPendingBashComponents();
378
378
 
379
- // Generate session title on first message
380
- const hasUserMessages = this.ctx.session.messages.some((m: AgentMessage) => m.role === "user");
381
- if (!hasUserMessages && !this.ctx.sessionManager.getSessionName() && !$env.PI_NO_TITLE) {
379
+ // Auto-generate a session title while the session is still unnamed.
380
+ // Greetings / acknowledgements / empty input carry no task, so they are
381
+ // skipped deterministically (no model invoked, no download-progress UI)
382
+ // and the session stays unnamed — the next user message gets a fresh
383
+ // chance, so titling defers past "hi" instead of latching onto it.
384
+ if (!this.ctx.sessionManager.getSessionName() && !$env.PI_NO_TITLE && !isLowSignalTitleInput(text)) {
382
385
  this.#showTinyTitleDownloadProgress(this.ctx.settings.get("providers.tinyModel"));
383
386
  const registry = this.ctx.session.modelRegistry;
384
387
  generateSessionTitle(
@@ -390,7 +393,9 @@ export class InputController {
390
393
  provider => this.ctx.session.agent.metadataForProvider(provider),
391
394
  )
392
395
  .then(async title => {
393
- if (title) {
396
+ // Re-check: a concurrent attempt for an earlier message may have
397
+ // already named the session. Don't clobber it.
398
+ if (title && !this.ctx.sessionManager.getSessionName()) {
394
399
  const applied = await this.ctx.sessionManager.setSessionName(title, "auto");
395
400
  if (applied) {
396
401
  setSessionTerminalTitle(
@@ -401,7 +406,13 @@ export class InputController {
401
406
  }
402
407
  }
403
408
  })
404
- .catch(() => {});
409
+ .catch(err => {
410
+ logger.warn("title-generator: uncaught auto-title error", {
411
+ sessionId: this.ctx.session.sessionId,
412
+ reason: "uncaught-auto-title-error",
413
+ error: err instanceof Error ? err.message : String(err),
414
+ });
415
+ });
405
416
  }
406
417
 
407
418
  if (this.ctx.onInputCallback) {
@@ -133,7 +133,11 @@ export class SelectorController {
133
133
  const availableWidth = this.ctx.editor.getTopBorderAvailableWidth(this.ctx.ui.terminal.columns);
134
134
  return this.ctx.statusLine.getTopBorder(availableWidth).content;
135
135
  },
136
- onPluginsChanged: () => {
136
+ onPluginsChanged: async () => {
137
+ const projectPath = await resolveActiveProjectRegistryPath(this.ctx.sessionManager.getCwd());
138
+ clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
139
+ await this.ctx.refreshSlashCommandState();
140
+ await this.ctx.session.refreshSshTool({ activateIfAvailable: true });
137
141
  this.ctx.ui.requestRender();
138
142
  },
139
143
  onCancel: () => {
@@ -184,16 +188,19 @@ export class SelectorController {
184
188
  */
185
189
  async showExtensionsDashboard(): Promise<void> {
186
190
  const dashboard = await ExtensionDashboard.create(getProjectDir(), this.ctx.settings, this.ctx.ui.terminal.rows);
187
- this.showSelector(done => {
188
- dashboard.onClose = () => {
189
- done();
190
- this.ctx.ui.requestRender();
191
- };
192
- dashboard.onRequestRender = () => {
193
- this.ctx.ui.requestRender();
194
- };
195
- return { component: dashboard, focus: dashboard };
191
+ const overlay = this.ctx.ui.showOverlay(dashboard, {
192
+ width: "100%",
193
+ maxHeight: "100%",
194
+ anchor: "top-left",
195
+ margin: 0,
196
196
  });
197
+ dashboard.onClose = () => {
198
+ overlay.hide();
199
+ this.ctx.ui.requestRender();
200
+ };
201
+ dashboard.onRequestRender = () => {
202
+ this.ctx.ui.requestRender();
203
+ };
197
204
  }
198
205
 
199
206
  /**
@@ -208,16 +215,19 @@ export class SelectorController {
208
215
  activeModelPattern,
209
216
  defaultModelPattern,
210
217
  });
211
- this.showSelector(done => {
212
- dashboard.onClose = () => {
213
- done();
214
- this.ctx.ui.requestRender();
215
- };
216
- dashboard.onRequestRender = () => {
217
- this.ctx.ui.requestRender();
218
- };
219
- return { component: dashboard, focus: dashboard };
218
+ const overlay = this.ctx.ui.showOverlay(dashboard, {
219
+ width: "100%",
220
+ maxHeight: "100%",
221
+ anchor: "top-left",
222
+ margin: 0,
220
223
  });
224
+ dashboard.onClose = () => {
225
+ overlay.hide();
226
+ this.ctx.ui.requestRender();
227
+ };
228
+ dashboard.onRequestRender = () => {
229
+ this.ctx.ui.requestRender();
230
+ };
221
231
  }
222
232
 
223
233
  /**
@@ -763,6 +773,7 @@ export class SelectorController {
763
773
  loadAllSessions: () => SessionManager.listAll(),
764
774
  allSessions,
765
775
  startInAllScope,
776
+ getTerminalRows: () => this.ctx.ui.terminal.rows,
766
777
  },
767
778
  );
768
779
  selector.setOnRequestRender(() => this.ctx.ui.requestRender());
@@ -607,7 +607,8 @@ export class InteractiveMode implements InteractiveModeContext {
607
607
  await this.initHooksAndCustomTools();
608
608
 
609
609
  // Restore mode from session (e.g. plan mode on resume)
610
- await this.#restoreModeFromSession();
610
+ this.session.setSessionSwitchReconciler?.(() => this.#reconcileModeFromSession());
611
+ await this.#reconcileModeFromSession();
611
612
 
612
613
  // Restore unsent editor draft from previous session shutdown (Ctrl+D).
613
614
  // One-shot: consumeDraft removes the sidecar after read so the next
@@ -1370,8 +1371,42 @@ export class InteractiveMode implements InteractiveModeContext {
1370
1371
  }
1371
1372
  }
1372
1373
 
1373
- /** Restore mode state from session entries on resume (e.g. plan mode). */
1374
- async #restoreModeFromSession(): Promise<void> {
1374
+ async #clearTransientModeState(): Promise<void> {
1375
+ if (this.planModeEnabled || this.planModePaused) {
1376
+ if (this.#planModePreviousTools !== undefined) {
1377
+ await this.session.setActiveToolsByName(this.#planModePreviousTools);
1378
+ }
1379
+ this.session.setStandingResolveHandler?.(null);
1380
+ this.session.setPlanModeState(undefined);
1381
+ this.planModeEnabled = false;
1382
+ this.planModePaused = false;
1383
+ this.planModePlanFilePath = undefined;
1384
+ this.#planModePreviousTools = undefined;
1385
+ this.#planModePreviousModelState = undefined;
1386
+ this.#pendingModelSwitch = undefined;
1387
+ this.#planModeHasEntered = false;
1388
+ this.#updatePlanModeStatus();
1389
+ }
1390
+
1391
+ if (this.goalModeEnabled || this.goalModePaused) {
1392
+ if (this.#goalModePreviousTools !== undefined) {
1393
+ await this.session.setActiveToolsByName(this.#goalModePreviousTools);
1394
+ }
1395
+ this.session.setGoalModeState(undefined);
1396
+ this.goalModeEnabled = false;
1397
+ this.goalModePaused = false;
1398
+ this.#goalModePreviousTools = undefined;
1399
+ this.#goalTurnHadToolCalls = false;
1400
+ this.#goalContinuationTurnInFlight = false;
1401
+ this.#goalSuppressNextContinuation = false;
1402
+ this.#cancelGoalContinuation();
1403
+ this.#updateGoalModeStatus();
1404
+ }
1405
+ }
1406
+
1407
+ /** Reconcile mode state from session entries on resume/switch. */
1408
+ async #reconcileModeFromSession(): Promise<void> {
1409
+ await this.#clearTransientModeState();
1375
1410
  const sessionContext = this.sessionManager.buildSessionContext();
1376
1411
  const goalEnabled = this.session.settings.get("goal.enabled");
1377
1412
  if (!goalEnabled && (sessionContext.mode === "goal" || sessionContext.mode === "goal_paused")) {
@@ -1,6 +1,6 @@
1
1
  import type { AuthStorage } from "@oh-my-pi/pi-ai";
2
2
  import type { OAuthProvider } from "@oh-my-pi/pi-ai/utils/oauth/types";
3
- import { Input, matchesKey, truncateToWidth } from "@oh-my-pi/pi-tui";
3
+ import { Input, matchesKey, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
4
4
  import { getAgentDbPath } from "@oh-my-pi/pi-utils";
5
5
  import { OAuthSelectorComponent } from "../../components/oauth-selector";
6
6
  import { theme } from "../../theme/theme";
@@ -15,6 +15,10 @@ const CALLBACK_SERVER_PROVIDERS: Partial<Record<OAuthProvider, true>> = {
15
15
  "google-antigravity": true,
16
16
  };
17
17
 
18
+ function loginUrlLink(url: string): string {
19
+ return `\x1b]8;;${url}\x07Open login URL\x1b]8;;\x07`;
20
+ }
21
+
18
22
  interface PromptState {
19
23
  message: string;
20
24
  placeholder?: string;
@@ -33,6 +37,7 @@ export class SignInTab implements SetupTab {
33
37
  #authStorage: AuthStorage;
34
38
  #selector: OAuthSelectorComponent;
35
39
  #statusLines: string[] = [];
40
+ #authUrl: string | undefined;
36
41
  #prompt: PromptState | undefined;
37
42
  #promptResolve: ((value: string) => void) | undefined;
38
43
  #loginAbort: AbortController | undefined;
@@ -72,22 +77,31 @@ export class SignInTab implements SetupTab {
72
77
  }
73
78
 
74
79
  render(width: number): string[] {
75
- const lines = [theme.fg("muted", "Pick a provider to sign in — you can connect more than one."), ""];
80
+ const lines: string[] = [];
76
81
  if (this.#loggingInProvider) {
77
- lines.push(theme.bold(`Signing in to ${this.#loggingInProvider}`), "");
82
+ lines.push(theme.bold(`Signing in to ${this.#loggingInProvider}`));
78
83
  } else {
84
+ lines.push(theme.fg("muted", "Pick a provider to sign in — you can connect more than one."), "");
79
85
  lines.push(...this.#selector.render(width));
80
86
  }
81
- if (this.#statusLines.length > 0) {
82
- lines.push("", ...this.#statusLines.map(line => truncateToWidth(line, width)));
87
+
88
+ const urlLines = this.#authUrl ? wrapTextWithAnsi(theme.fg("dim", this.#authUrl), width) : [];
89
+ if (this.#authUrl) {
90
+ lines.push(theme.fg("accent", `Browser login: ${loginUrlLink(this.#authUrl)}`), ...urlLines.slice(0, 2));
83
91
  }
84
92
  if (this.#prompt) {
85
- lines.push("", theme.fg("warning", this.#prompt.message));
93
+ lines.push(theme.fg("warning", this.#prompt.message));
86
94
  if (this.#prompt.placeholder) {
87
95
  lines.push(theme.fg("dim", this.#prompt.placeholder));
88
96
  }
89
97
  lines.push(this.#prompt.input.render(width)[0] ?? "");
90
98
  }
99
+ if (urlLines.length > 2) {
100
+ lines.push(...urlLines);
101
+ }
102
+ if (this.#statusLines.length > 0) {
103
+ lines.push(...this.#statusLines.flatMap(line => wrapTextWithAnsi(line, width)));
104
+ }
91
105
  return lines;
92
106
  }
93
107
 
@@ -109,6 +123,7 @@ export class SignInTab implements SetupTab {
109
123
  this.#selector.stopValidation();
110
124
  this.#loggingInProvider = providerId;
111
125
  this.#statusLines = [theme.fg("dim", "Starting OAuth flow…")];
126
+ this.#authUrl = undefined;
112
127
  this.#loginAbort = new AbortController();
113
128
  this.host.restoreFocus();
114
129
  this.host.requestRender();
@@ -116,7 +131,8 @@ export class SignInTab implements SetupTab {
116
131
  await this.#authStorage.login(providerId as OAuthProvider, {
117
132
  signal: this.#loginAbort.signal,
118
133
  onAuth: info => {
119
- this.#statusLines.push(theme.fg("accent", `Open this URL: ${info.url}`));
134
+ this.#authUrl = info.url;
135
+ this.#statusLines = [];
120
136
  if (info.instructions) {
121
137
  this.#statusLines.push(theme.fg("warning", info.instructions));
122
138
  }
@@ -140,6 +156,7 @@ export class SignInTab implements SetupTab {
140
156
  theme.fg("success", `${theme.status.success} Signed in to ${providerId}`),
141
157
  theme.fg("dim", `Credentials saved to ${getAgentDbPath()}`),
142
158
  ];
159
+ this.#authUrl = undefined;
143
160
  this.#loggingInProvider = undefined;
144
161
  this.#loginAbort = undefined;
145
162
  this.#selector.stopValidation();
@@ -150,12 +167,14 @@ export class SignInTab implements SetupTab {
150
167
  if (this.#disposed) return;
151
168
  if (this.#loginAbort?.signal.aborted) {
152
169
  this.#statusLines = [theme.fg("dim", "Login cancelled.")];
170
+ this.#authUrl = undefined;
153
171
  } else {
154
172
  const message = error instanceof Error ? error.message : String(error);
155
173
  this.#statusLines = [
156
174
  theme.fg("error", `Login failed: ${message}`),
157
175
  theme.fg("dim", "Choose another provider or press Esc to continue."),
158
176
  ];
177
+ this.#authUrl = undefined;
159
178
  }
160
179
  this.#loggingInProvider = undefined;
161
180
  this.#loginAbort = undefined;
@@ -174,6 +193,7 @@ export class SignInTab implements SetupTab {
174
193
  this.#resolvePrompt(value);
175
194
  };
176
195
  input.onEscape = () => {
196
+ this.#loginAbort?.abort();
177
197
  this.#resolvePrompt("");
178
198
  };
179
199
  this.host.setFocus(input);
@@ -31,6 +31,16 @@ export function matchesSelectDown(data: string): boolean {
31
31
  return getKeybindings().matches(data, "tui.select.down");
32
32
  }
33
33
 
34
+ /** Match the generic selector page-up keybinding. */
35
+ export function matchesSelectPageUp(data: string): boolean {
36
+ return getKeybindings().matches(data, "tui.select.pageUp");
37
+ }
38
+
39
+ /** Match the generic selector page-down keybinding. */
40
+ export function matchesSelectPageDown(data: string): boolean {
41
+ return getKeybindings().matches(data, "tui.select.pageDown");
42
+ }
43
+
34
44
  export function matchesAppExternalEditor(data: string): boolean {
35
45
  const keybindings = getKeybindings();
36
46
  const externalEditorKeys = keybindings.getKeys("app.editor.external");
@@ -4,6 +4,7 @@ description: Fast read-only codebase scout returning compressed context for hand
4
4
  tools: read, search, find, web_search
5
5
  model: pi/smol
6
6
  thinking-level: med
7
+ read-summarize: false
7
8
  output:
8
9
  properties:
9
10
  summary:
@@ -4,6 +4,7 @@ description: Researches external libraries and APIs by reading source code. Retu
4
4
  tools: read, search, find, bash, lsp, web_search, ast_grep
5
5
  model: pi/smol
6
6
  thinking-level: minimal
7
+ read-summarize: false
7
8
  output:
8
9
  properties:
9
10
  answer:
@@ -0,0 +1,8 @@
1
+ Write a 20-line poem about balancing OAuth accounts across many providers.
2
+
3
+ Form:
4
+ - Exactly 20 lines, no title, no stanza breaks.
5
+ - Each line is terse and image-driven, in the spirit of haiku: 7 words or fewer, no end punctuation.
6
+ - Let the imagery carry the theme — tokens, scopes, refresh cycles, expiry, consent, revocation — rather than naming them literally.
7
+
8
+ Output only the 20 lines. No preamble, no commentary, no code fences.
@@ -0,0 +1,10 @@
1
+ <user_interjection>
2
+ The user sent this message while you were working on the current task. It takes
3
+ priority and supersedes your earlier plan wherever they conflict. Stop work that no
4
+ longer matches their intent, re-read the request below, and adjust what you are doing
5
+ now.
6
+
7
+ <message>
8
+ {{message}}
9
+ </message>
10
+ </user_interjection>
@@ -28,38 +28,13 @@ When a user describes what they want an agent to do:
28
28
  - MUST clearly indicate the agent's primary function
29
29
  - SHOULD be memorable and easy to type
30
30
  - NEVER use generic terms like "helper" or "assistant"
31
- 6. Example agent descriptions
32
- - In the `whenToUse` field, SHOULD include examples of when this agent SHOULD be used
33
- - Format examples as:
34
- ```
35
- <example>
36
- Context: The user is creating a test-runner agent that should be called after a logical chunk of code is written.
37
- user: "Please write a function that checks if a number is prime"
38
- assistant: "Here is the relevant function: "
39
- <function call omitted for brevity only for this example>
40
- <commentary>
41
- Since a significant piece of code was written, use the {{TASK_TOOL_NAME}} tool to launch the test-runner agent to run the tests.
42
- </commentary>
43
- assistant: "Now let me use the test-runner agent to run the tests"
44
- </example>
45
- <example>
46
- Context: User is creating an agent to respond to the word "hello" with a friendly joke.
47
- user: "Hello"
48
- assistant: "I'm going to use the {{TASK_TOOL_NAME}} tool to launch the greeting-responder agent to respond with a friendly joke"
49
- <commentary>
50
- Since the user is greeting, use the greeting-responder agent to respond with a friendly joke.
51
- </commentary>
52
- </example>
53
- ```
54
- - If the user mentioned or implied proactive use, SHOULD include proactive examples
55
- - MUST ensure examples show the assistant using the Agent tool, not responding directly
56
31
 
57
32
  Your output MUST be a valid JSON object with exactly these fields:
58
33
 
59
34
  ```json
60
35
  {
61
36
  "identifier": "A unique, descriptive identifier using lowercase letters, numbers, and hyphens (e.g., 'test-runner', 'api-docs-writer', 'code-formatter')",
62
- "whenToUse": "A precise, actionable description starting with 'Use this agent when…' that clearly defines the triggering conditions and use cases. Include examples as described above.",
37
+ "whenToUse": "A precise, single-sentence trigger description starting with 'Use this agent when…' that defines the conditions and use cases. Keep it concise and self-contained — NEVER embed <example>/<commentary> blocks, multi-turn transcripts, or escaped newlines.",
63
38
  "systemPrompt": "The complete system prompt that will govern the agent's behavior, written in second person ('You are…', 'You will…') and structured for maximum clarity and effectiveness"
64
39
  }
65
40
  ```