@oh-my-pi/pi-coding-agent 16.4.8 → 16.5.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.
Files changed (134) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/cli.js +3502 -3444
  3. package/dist/types/cli/args.d.ts +5 -0
  4. package/dist/types/cli/gallery-fixtures/shell.d.ts +1 -1
  5. package/dist/types/commands/launch.d.ts +15 -0
  6. package/dist/types/config/model-resolver.d.ts +4 -3
  7. package/dist/types/config/model-roles.d.ts +8 -0
  8. package/dist/types/config/settings-schema.d.ts +46 -14
  9. package/dist/types/extensibility/extensions/types.d.ts +1 -1
  10. package/dist/types/launch/broker.d.ts +2 -0
  11. package/dist/types/launch/client.d.ts +22 -0
  12. package/dist/types/launch/paths.d.ts +4 -0
  13. package/dist/types/launch/presence.d.ts +8 -0
  14. package/dist/types/launch/protocol.d.ts +170 -0
  15. package/dist/types/launch/terminal-output.d.ts +7 -0
  16. package/dist/types/modes/components/agent-hub.d.ts +1 -1
  17. package/dist/types/modes/components/index.d.ts +1 -0
  18. package/dist/types/modes/components/model-browser.d.ts +17 -2
  19. package/dist/types/modes/components/model-hub.d.ts +0 -10
  20. package/dist/types/modes/components/model-picker.d.ts +44 -0
  21. package/dist/types/modes/components/status-line/types.d.ts +3 -0
  22. package/dist/types/modes/print-mode.d.ts +14 -8
  23. package/dist/types/modes/print-mode.test.d.ts +1 -0
  24. package/dist/types/modes/theme/theme.d.ts +2 -1
  25. package/dist/types/sdk.d.ts +5 -1
  26. package/dist/types/session/agent-session.d.ts +42 -0
  27. package/dist/types/session/session-context.d.ts +9 -0
  28. package/dist/types/session/session-entries.d.ts +6 -0
  29. package/dist/types/thinking.d.ts +2 -2
  30. package/dist/types/tiny/models.d.ts +1 -1
  31. package/dist/types/tools/browser/launch.d.ts +1 -0
  32. package/dist/types/tools/browser/run-cancellation.d.ts +28 -2
  33. package/dist/types/tools/browser/tab-protocol.d.ts +6 -0
  34. package/dist/types/tools/browser/tab-worker.d.ts +6 -0
  35. package/dist/types/tools/builtin-names.d.ts +1 -1
  36. package/dist/types/tools/index.d.ts +1 -0
  37. package/dist/types/tools/launch.d.ts +121 -0
  38. package/dist/types/tools/render-utils.d.ts +2 -0
  39. package/dist/types/tools/terminal-output.d.ts +5 -0
  40. package/dist/types/vibe/runtime.d.ts +2 -2
  41. package/dist/types/web/search/types.d.ts +0 -8
  42. package/package.json +20 -20
  43. package/src/cli/args.ts +11 -0
  44. package/src/cli/flag-tables.ts +9 -0
  45. package/src/cli/gallery-fixtures/shell.ts +82 -1
  46. package/src/cli.ts +10 -0
  47. package/src/commands/launch.ts +17 -0
  48. package/src/config/model-resolver.ts +110 -31
  49. package/src/config/model-roles.ts +14 -0
  50. package/src/config/settings-schema.ts +71 -7
  51. package/src/edit/renderer.ts +13 -10
  52. package/src/eval/__tests__/agent-bridge.test.ts +2 -2
  53. package/src/eval/__tests__/completion-bridge.test.ts +1 -1
  54. package/src/eval/completion-bridge.ts +4 -4
  55. package/src/export/ttsr.ts +0 -3
  56. package/src/extensibility/extensions/model-api.ts +1 -1
  57. package/src/extensibility/extensions/types.ts +1 -1
  58. package/src/internal-urls/docs-index.ts +4 -4
  59. package/src/launch/broker.ts +1017 -0
  60. package/src/launch/client.ts +344 -0
  61. package/src/launch/paths.ts +17 -0
  62. package/src/launch/presence.ts +82 -0
  63. package/src/launch/protocol.ts +386 -0
  64. package/src/launch/terminal-output.ts +46 -0
  65. package/src/main.ts +50 -1
  66. package/src/modes/acp/acp-agent.ts +8 -1
  67. package/src/modes/components/agent-hub.ts +101 -31
  68. package/src/modes/components/compaction-summary-message.ts +8 -2
  69. package/src/modes/components/index.ts +1 -0
  70. package/src/modes/components/model-browser.ts +111 -21
  71. package/src/modes/components/model-hub.ts +23 -116
  72. package/src/modes/components/model-picker.ts +233 -0
  73. package/src/modes/components/snapcompact-shape-preview-doc.md +7 -11
  74. package/src/modes/components/status-line/component.test.ts +41 -2
  75. package/src/modes/components/status-line/component.ts +4 -0
  76. package/src/modes/components/status-line/segments.ts +6 -0
  77. package/src/modes/components/status-line/types.ts +3 -0
  78. package/src/modes/controllers/command-controller.ts +9 -1
  79. package/src/modes/controllers/event-controller.ts +31 -32
  80. package/src/modes/controllers/selector-controller.ts +96 -22
  81. package/src/modes/controllers/tan-command-controller.ts +40 -1
  82. package/src/modes/interactive-mode.ts +7 -3
  83. package/src/modes/print-mode.test.ts +71 -0
  84. package/src/modes/print-mode.ts +51 -2
  85. package/src/modes/theme/theme.ts +9 -0
  86. package/src/modes/utils/ui-helpers.ts +25 -4
  87. package/src/prompts/agents/designer.md +1 -1
  88. package/src/prompts/agents/librarian.md +1 -1
  89. package/src/prompts/agents/reviewer.md +1 -1
  90. package/src/prompts/agents/scout.md +1 -1
  91. package/src/prompts/system/plan-yolo-handoff.md +5 -0
  92. package/src/prompts/system/prewalk-checklist.md +7 -0
  93. package/src/prompts/system/prewalk-continue.md +1 -0
  94. package/src/prompts/system/prewalk-plan.md +13 -0
  95. package/src/prompts/system/system-prompt.md +9 -7
  96. package/src/prompts/system/tan-context-switch.md +17 -0
  97. package/src/prompts/tools/bash.md +6 -4
  98. package/src/prompts/tools/browser.md +4 -4
  99. package/src/prompts/tools/launch.md +25 -0
  100. package/src/sdk.ts +8 -2
  101. package/src/session/agent-session.ts +550 -87
  102. package/src/session/session-context.test.ts +10 -5
  103. package/src/session/session-context.ts +25 -8
  104. package/src/session/session-entries.ts +6 -0
  105. package/src/slash-commands/builtin-registry.ts +25 -0
  106. package/src/task/agents.ts +2 -2
  107. package/src/thinking.ts +10 -3
  108. package/src/tiny/models.ts +7 -7
  109. package/src/tools/bash-interactive.ts +5 -8
  110. package/src/tools/bash.ts +38 -24
  111. package/src/tools/browser/cmux/cmux-tab.ts +17 -2
  112. package/src/tools/browser/launch.ts +8 -4
  113. package/src/tools/browser/run-cancellation.ts +66 -6
  114. package/src/tools/browser/tab-protocol.ts +6 -0
  115. package/src/tools/browser/tab-supervisor.ts +17 -1
  116. package/src/tools/browser/tab-worker.ts +140 -21
  117. package/src/tools/builtin-names.ts +1 -0
  118. package/src/tools/eval-render.ts +16 -14
  119. package/src/tools/index.ts +5 -0
  120. package/src/tools/inspect-image.ts +2 -2
  121. package/src/tools/launch.ts +643 -0
  122. package/src/tools/render-utils.ts +3 -0
  123. package/src/tools/renderers.ts +2 -0
  124. package/src/tools/terminal-output.ts +141 -0
  125. package/src/tts/speech-enhancer.ts +2 -2
  126. package/src/utils/image-vision-fallback.ts +3 -3
  127. package/src/vibe/runtime.ts +2 -2
  128. package/src/web/search/provider.ts +0 -10
  129. package/src/web/search/providers/public.ts +2 -4
  130. package/src/web/search/types.ts +0 -10
  131. package/dist/types/web/search/providers/bing.d.ts +0 -14
  132. package/dist/types/web/search/providers/yahoo.d.ts +0 -14
  133. package/src/web/search/providers/bing.ts +0 -197
  134. package/src/web/search/providers/yahoo.ts +0 -179
@@ -1,4 +1,4 @@
1
- /** Gallery fixtures for the shell tools (bash, eval). */
1
+ /** Gallery fixtures for the shell tools (bash, eval, launch). */
2
2
  import type { GalleryFixture } from "./types";
3
3
 
4
4
  export const shellFixtures: Record<string, GalleryFixture> = {
@@ -56,6 +56,87 @@ export const shellFixtures: Record<string, GalleryFixture> = {
56
56
  },
57
57
  },
58
58
 
59
+ launch: {
60
+ label: "Launch",
61
+ streamingArgs: { op: "start", name: "web" },
62
+ args: {
63
+ op: "start",
64
+ name: "web",
65
+ application: "bun",
66
+ args: ["run", "dev"],
67
+ ready: { log: "Local:.*http", port: 5173, timeout: 30 },
68
+ },
69
+ result: {
70
+ content: [
71
+ {
72
+ type: "text",
73
+ text: "Started web: ready pid=51234 uptime=1.2s restarts=0\nReady: Local: http://localhost:5173",
74
+ },
75
+ ],
76
+ details: {
77
+ op: "start",
78
+ daemon: {
79
+ name: "web",
80
+ id: "d-1",
81
+ state: "ready",
82
+ pid: 51234,
83
+ createdAt: 0,
84
+ startedAt: Date.now() - 1_200,
85
+ readyAt: Date.now(),
86
+ restartCount: 0,
87
+ outputBytes: 2048,
88
+ readyMatch: "Local: http://localhost:5173/",
89
+ persist: false,
90
+ detached: false,
91
+ },
92
+ timedOut: false,
93
+ },
94
+ },
95
+ errorResult: {
96
+ content: [{ type: "text", text: "start requires application" }],
97
+ isError: true,
98
+ details: { op: "start" },
99
+ },
100
+ },
101
+
102
+ launch_logs: {
103
+ label: "Launch",
104
+ renderer: "launch",
105
+ args: { op: "logs", name: "comp-debug", lines: 100, follow: true, cursor: 233_512, timeout: 30 },
106
+ result: {
107
+ content: [
108
+ {
109
+ type: "text",
110
+ text: [
111
+ "Breakpoint 1: 3 locations.",
112
+ "(lldb) run",
113
+ "Process 726 launched: '/tmp/compiler'",
114
+ "frame #0: 0x0000000100012f80 compiler`parse_expression",
115
+ "[comp-debug: ready; cursor=233797]",
116
+ ].join("\n"),
117
+ },
118
+ ],
119
+ details: {
120
+ op: "logs",
121
+ cursor: 233_797,
122
+ timedOut: false,
123
+ state: "ready",
124
+ terminalRows: [
125
+ "\x1b[0mBreakpoint 1: 3 locations.",
126
+ "\x1b[0m(lldb) run",
127
+ "\x1b[0mProcess 726 launched: '/tmp/compiler'",
128
+ "\x1b[0mframe #0: 0x0000000100012f80 compiler`parse_expression",
129
+ "\x1b[0m\x1b[1;38;5;2m(lldb)\x1b[0m ",
130
+ ],
131
+ },
132
+ },
133
+ errorResult: {
134
+ content: [{ type: "text", text: "No daemon named web" }],
135
+ isError: true,
136
+ details: { op: "logs" },
137
+ },
138
+ },
139
+
59
140
  eval: {
60
141
  label: "Eval",
61
142
  streamingArgs: {
package/src/cli.ts CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  import { declareWorkerHostEntry, installWorkerInbox } from "@oh-my-pi/pi-utils/worker-host";
28
28
  import { installProfileAlias, resolveProfileAliasCommandFromProcess } from "./cli/profile-alias";
29
29
  import { extractProfileFlags } from "./cli/profile-bootstrap";
30
+ import { DAEMON_BROKER_WORKER_ARG } from "./launch/protocol";
30
31
 
31
32
  if (Bun.semver.order(Bun.version, MIN_BUN_VERSION) < 0) {
32
33
  process.stderr.write(
@@ -78,6 +79,8 @@ async function runSmokeTest(): Promise<void> {
78
79
  const { smokeTestTtsWorker } = await import("./tts/tts-client");
79
80
  const { smokeTestMnemopiEmbedWorker } = await import("./mnemopi/embed-client");
80
81
  const { smokeTestJsEvalWorker } = await import("./eval/js/context-manager");
82
+ // Smoke dependencies stay lazy so normal CLI startup does not load worker clients.
83
+ const { smokeTestDaemonBroker } = await import("./launch/client");
81
84
  await smokeTestSyncWorker();
82
85
 
83
86
  const statsServer = await startServer(0);
@@ -97,6 +100,7 @@ async function runSmokeTest(): Promise<void> {
97
100
  await smokeTestJsEvalWorker();
98
101
  await smokeTestTtsWorker();
99
102
  await smokeTestMnemopiEmbedWorker();
103
+ await smokeTestDaemonBroker();
100
104
  process.stdout.write("smoke-test: ok\n");
101
105
  }
102
106
 
@@ -167,6 +171,12 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
167
171
  await runIpcSubprocessWorker(startMnemopiEmbedWorker);
168
172
  return true;
169
173
  }
174
+ if (arg === DAEMON_BROKER_WORKER_ARG) {
175
+ // Worker selectors must dispatch before the normal command graph loads.
176
+ const { startDaemonBrokerFromEnvironment } = await import("./launch/broker");
177
+ await startDaemonBrokerFromEnvironment();
178
+ return true;
179
+ }
170
180
  return false;
171
181
  }
172
182
 
@@ -34,6 +34,23 @@ export default class Index extends Command {
34
34
  plan: Flags.string({
35
35
  description: "Plan model for architectural planning (or PI_PLAN_MODEL env)",
36
36
  }),
37
+ prewalk: Flags.boolean({
38
+ description:
39
+ "Switch from the active model to a fast/cheap model at the first edit/write after the plan's todo list exists (default off; see prewalk.enabled)",
40
+ }),
41
+ "no-prewalk": Flags.boolean({
42
+ description: "Disable prewalk even if prewalk.enabled is set",
43
+ }),
44
+ "prewalk-into": Flags.string({
45
+ description: 'Target model for prewalk (default the "smol" role)',
46
+ }),
47
+ "plan-yolo": Flags.boolean({
48
+ description:
49
+ "Force read-only plan mode at start, auto-approve the plan on the model's first resolve call, then switch to --plan-yolo-into to implement it",
50
+ }),
51
+ "plan-yolo-into": Flags.string({
52
+ description: 'Target model for plan-yolo execution (default the "smol" role)',
53
+ }),
37
54
  provider: Flags.string({
38
55
  description: "Provider to use (legacy; prefer --model)",
39
56
  }),
@@ -37,7 +37,14 @@ import {
37
37
  resolveThinkingLevelForModel,
38
38
  } from "../thinking";
39
39
  import { isAuthenticated, kNoAuth, type ModelRegistry } from "./model-registry";
40
- import { MODEL_ROLE_IDS, type ModelRole } from "./model-roles";
40
+ import {
41
+ DEFAULT_MODEL_ROLE_ALIAS,
42
+ formatModelRoleAlias,
43
+ LEGACY_MODEL_ROLE_ALIAS_PREFIX,
44
+ MODEL_ROLE_ALIAS_PREFIX,
45
+ MODEL_ROLE_IDS,
46
+ type ModelRole,
47
+ } from "./model-roles";
41
48
  import type { Settings } from "./settings";
42
49
 
43
50
  function isKnownProvider(provider: string): provider is KnownProvider {
@@ -109,8 +116,7 @@ function parseThinkingSuffix(value: string, options?: ThinkingSuffixOptions): Co
109
116
  * level / `:auto` sentinel); `base` then has the suffix stripped. Otherwise
110
117
  * `base` is the input.
111
118
  * `minColonIndex` requires the colon to appear strictly after that index —
112
- * role-alias callers pass `PREFIX_MODEL_ROLE.length` so the base is at least
113
- * as long as the `pi/` prefix.
119
+ * role-alias callers pass the matched alias prefix length.
114
120
  */
115
121
  function splitThinkingSuffix(
116
122
  pattern: string,
@@ -850,17 +856,32 @@ export function parseModelPattern(
850
856
  );
851
857
  }
852
858
 
853
- const PREFIX_MODEL_ROLE = "pi/";
854
859
  const DEFAULT_MODEL_ROLE = "default";
860
+ const MODEL_ROLE_ALIAS_PREFIXES = [MODEL_ROLE_ALIAS_PREFIX, LEGACY_MODEL_ROLE_ALIAS_PREFIX];
861
+
862
+ function isModelRole(role: string): role is ModelRole {
863
+ return (MODEL_ROLE_IDS as string[]).includes(role);
864
+ }
865
+
866
+ /**
867
+ * Minimum colon index for splitting a `:<level>` suffix off a role alias, or
868
+ * `undefined` when `value` is not role-alias shaped. Doubles as the slice
869
+ * offset of the role name for prefixed aliases (`@role`, `pi/role`); the bare
870
+ * `*` default alias returns 0 because its colon sits immediately after the
871
+ * one-character token (`*:xhigh`).
872
+ */
873
+ function modelRoleAliasPrefixLength(value: string): number | undefined {
874
+ if (value === DEFAULT_MODEL_ROLE_ALIAS || value.startsWith(`${DEFAULT_MODEL_ROLE_ALIAS}:`)) return 0;
875
+ return MODEL_ROLE_ALIAS_PREFIXES.find(prefix => value.startsWith(prefix))?.length;
876
+ }
855
877
 
856
- function getModelRoleAlias(value: string): ModelRole | undefined {
878
+ function getModelRoleAlias(value: string, settings?: Settings): string | undefined {
857
879
  const normalized = value.trim();
858
- if (!normalized.startsWith(PREFIX_MODEL_ROLE)) return undefined;
880
+ const prefixLength = modelRoleAliasPrefixLength(normalized);
881
+ if (prefixLength === undefined) return undefined;
859
882
 
860
- const candidate = normalized.slice(PREFIX_MODEL_ROLE.length);
861
- for (const role of MODEL_ROLE_IDS) {
862
- if (candidate === role) return role;
863
- }
883
+ const candidate = normalized === DEFAULT_MODEL_ROLE_ALIAS ? DEFAULT_MODEL_ROLE : normalized.slice(prefixLength);
884
+ if (isModelRole(candidate) || settings?.getModelRole(candidate) !== undefined) return candidate;
864
885
  return undefined;
865
886
  }
866
887
 
@@ -871,7 +892,14 @@ function normalizeModelPatternList(value: string | string[] | undefined): string
871
892
  }
872
893
 
873
894
  function isSessionInheritedAgentPattern(value: string): boolean {
874
- return value === DEFAULT_MODEL_ROLE || value === `${PREFIX_MODEL_ROLE}${DEFAULT_MODEL_ROLE}` || value === "pi/task";
895
+ return (
896
+ value === DEFAULT_MODEL_ROLE ||
897
+ value === formatModelRoleAlias(DEFAULT_MODEL_ROLE) ||
898
+ value === DEFAULT_MODEL_ROLE_ALIAS ||
899
+ value === `${LEGACY_MODEL_ROLE_ALIAS_PREFIX}${DEFAULT_MODEL_ROLE}` ||
900
+ value === formatModelRoleAlias("task") ||
901
+ value === `${LEGACY_MODEL_ROLE_ALIAS_PREFIX}task`
902
+ );
875
903
  }
876
904
 
877
905
  function shouldInheritDefaultBeforePriority(role: ModelRole): boolean {
@@ -904,7 +932,7 @@ function resolveDefaultInheritedPatterns(
904
932
  configuredDefault: string | undefined,
905
933
  roleDefaults: string[],
906
934
  settings: Settings | undefined,
907
- visited: Set<ModelRole>,
935
+ visited: Set<string>,
908
936
  ): string[] {
909
937
  if (!shouldInheritDefaultBeforePriority(role) || !configuredDefault) return [];
910
938
 
@@ -912,13 +940,12 @@ function resolveDefaultInheritedPatterns(
912
940
  for (const pattern of normalizeModelPatternList(configuredDefault)) {
913
941
  const { base: aliasCandidate, level: thinkingLevel } = splitThinkingSuffix(
914
942
  pattern,
915
- PREFIX_MODEL_ROLE.length,
943
+ modelRoleAliasPrefixLength(pattern) ?? LEGACY_MODEL_ROLE_ALIAS_PREFIX.length,
916
944
  MAX_THINKING_SUFFIX_OPTIONS,
917
945
  );
918
- const aliasRole = getModelRoleAlias(aliasCandidate);
946
+ const aliasRole = getModelRoleAlias(aliasCandidate, settings);
919
947
  if (aliasRole === role) {
920
- // Self-alias (e.g. modelRoles.default = "pi/smol") would loop back to the
921
- // same unset role; collapse straight to the built-in priority chain.
948
+ // Self-alias (e.g. modelRoles.default = "@smol") would loop back to the
922
949
  resolved.push(
923
950
  ...(thinkingLevel
924
951
  ? roleDefaults.map(defaultPattern => `${defaultPattern}:${thinkingLevel}`)
@@ -927,8 +954,7 @@ function resolveDefaultInheritedPatterns(
927
954
  continue;
928
955
  }
929
956
  if (aliasRole && !visited.has(aliasRole)) {
930
- // Cross-role alias (e.g. modelRoles.default = "pi/slow"): resolve the
931
- // target role's patterns now so downstream one-layer expanders see
957
+ // Cross-role alias (e.g. modelRoles.default = "@slow"): resolve the
932
958
  // concrete model patterns instead of another role alias.
933
959
  const recursed = resolveConfiguredRolePattern(pattern, settings, new Set(visited));
934
960
  if (recursed && recursed.length > 0) {
@@ -944,27 +970,29 @@ function resolveDefaultInheritedPatterns(
944
970
  function resolveConfiguredRolePattern(
945
971
  value: string,
946
972
  settings?: Settings,
947
- visited: Set<ModelRole> = new Set(),
973
+ visited: Set<string> = new Set(),
948
974
  ): string[] | undefined {
949
975
  const normalized = value.trim();
950
976
  if (!normalized) return undefined;
951
977
 
952
978
  const { base: aliasCandidate, level: thinkingLevel } = splitThinkingSuffix(
953
979
  normalized,
954
- PREFIX_MODEL_ROLE.length,
980
+ modelRoleAliasPrefixLength(normalized) ?? LEGACY_MODEL_ROLE_ALIAS_PREFIX.length,
955
981
  MAX_THINKING_SUFFIX_OPTIONS,
956
982
  );
957
- const role = getModelRoleAlias(aliasCandidate);
983
+ const role = getModelRoleAlias(aliasCandidate, settings);
958
984
  if (!role) return [normalized];
959
985
  if (visited.has(role)) return undefined;
960
986
  visited.add(role);
961
987
 
962
988
  const configured = settings?.getModelRole(role)?.trim();
963
989
  const configuredDefault = settings?.getModelRole(DEFAULT_MODEL_ROLE)?.trim();
964
- const roleDefaults = rolePriorityDefaults(role);
990
+ const roleDefaults = isModelRole(role) ? rolePriorityDefaults(role) : [];
965
991
  const resolved = configured
966
992
  ? normalizeModelPatternList(configured)
967
- : resolveDefaultInheritedPatterns(role, configuredDefault, roleDefaults, settings, visited);
993
+ : isModelRole(role)
994
+ ? resolveDefaultInheritedPatterns(role, configuredDefault, roleDefaults, settings, visited)
995
+ : roleDefaults;
968
996
  if (resolved.length === 0) {
969
997
  resolved.push(...roleDefaults);
970
998
  }
@@ -976,7 +1004,7 @@ function resolveConfiguredRolePattern(
976
1004
  }
977
1005
 
978
1006
  /**
979
- * Expand a role alias like "pi/smol" to the configured model string.
1007
+ * Expand a role alias like "@smol" to the configured model string.
980
1008
  */
981
1009
  export function expandRoleAlias(value: string, settings?: Settings): string {
982
1010
  const normalized = value.trim();
@@ -1014,8 +1042,13 @@ export function resolveAgentModelPatterns(options: AgentModelPatternResolutionOp
1014
1042
  const singleAgentPattern = normalizedAgentPatterns.length === 1 ? normalizedAgentPatterns[0] : undefined;
1015
1043
  const agentInheritsSessionModel = singleAgentPattern ? isSessionInheritedAgentPattern(singleAgentPattern) : false;
1016
1044
  if (configuredAgentPatterns.length > 0) {
1045
+ if (
1046
+ singleAgentPattern === formatModelRoleAlias("task") ||
1047
+ singleAgentPattern === `${LEGACY_MODEL_ROLE_ALIAS_PREFIX}task`
1048
+ ) {
1049
+ return configuredAgentPatterns;
1050
+ }
1017
1051
  if (!agentInheritsSessionModel) return configuredAgentPatterns;
1018
- if (singleAgentPattern === "pi/task") return configuredAgentPatterns;
1019
1052
  }
1020
1053
 
1021
1054
  const fallback =
@@ -1102,12 +1135,16 @@ export function extractExplicitThinkingSelector(
1102
1135
  let current = normalized;
1103
1136
  while (!visited.has(current)) {
1104
1137
  visited.add(current);
1105
- const strictSelector = splitThinkingSuffix(current, PREFIX_MODEL_ROLE.length).level;
1138
+ const rolePrefixLength = modelRoleAliasPrefixLength(current) ?? LEGACY_MODEL_ROLE_ALIAS_PREFIX.length;
1139
+ const strictSelector = splitThinkingSuffix(current, rolePrefixLength).level;
1106
1140
  if (strictSelector) {
1107
1141
  return strictSelector;
1108
1142
  }
1109
- const maxSelector = splitThinkingSuffix(current, PREFIX_MODEL_ROLE.length, MAX_THINKING_SUFFIX_OPTIONS).level;
1110
- if (maxSelector && (current.startsWith(PREFIX_MODEL_ROLE) || !isLiteralModelSelector(current, options))) {
1143
+ const maxSelector = splitThinkingSuffix(current, rolePrefixLength, MAX_THINKING_SUFFIX_OPTIONS).level;
1144
+ if (
1145
+ maxSelector &&
1146
+ (modelRoleAliasPrefixLength(current) !== undefined || !isLiteralModelSelector(current, options))
1147
+ ) {
1111
1148
  return maxSelector;
1112
1149
  }
1113
1150
  const expanded = expandRoleAlias(current, settings).trim();
@@ -1278,7 +1315,7 @@ export function resolveAdvisorRoleSelection(
1278
1315
  settings: Settings,
1279
1316
  availableModels: Model<Api>[],
1280
1317
  ): { model: Model<Api>; thinkingLevel?: ConfiguredThinkingLevel } | undefined {
1281
- const resolved = resolveModelRoleValue(`${PREFIX_MODEL_ROLE}advisor`, availableModels, {
1318
+ const resolved = resolveModelRoleValue(formatModelRoleAlias("advisor"), availableModels, {
1282
1319
  settings,
1283
1320
  matchPreferences: getModelMatchPreferences(settings),
1284
1321
  });
@@ -1300,6 +1337,7 @@ export async function resolveModelScope(
1300
1337
  patterns: string[],
1301
1338
  modelRegistry: Pick<ModelRegistry, "getAvailable">,
1302
1339
  preferences?: ModelMatchPreferences,
1340
+ settings?: Settings,
1303
1341
  ): Promise<ScopedModel[]> {
1304
1342
  const availableModels = modelRegistry.getAvailable();
1305
1343
  const context = buildPreferenceContext(availableModels, preferences);
@@ -1337,6 +1375,25 @@ export async function resolveModelScope(
1337
1375
  continue;
1338
1376
  }
1339
1377
 
1378
+ // Role aliases (`@smol`, `pi/slow`) resolve to the role's single concrete
1379
+ // model — not its whole fallback chain — so a role contributes one scope
1380
+ // entry exactly like `--model` would pick. (Bare `*` stays a match-all
1381
+ // glob above; scope semantics, not the default-role alias.)
1382
+ if (settings && modelRoleAliasPrefixLength(pattern) !== undefined) {
1383
+ const resolved = resolveModelRoleValue(pattern, availableModels, { settings, matchPreferences: preferences });
1384
+ if (resolved.warning) logger.warn(resolved.warning);
1385
+ if (!resolved.model) {
1386
+ logger.warn(`No models match pattern "${pattern}"`);
1387
+ continue;
1388
+ }
1389
+ if (resolved.thinkingLevel === AUTO_THINKING) {
1390
+ addScopedModel(resolved.model, undefined, false);
1391
+ } else {
1392
+ addScopedModel(resolved.model, resolved.thinkingLevel, resolved.explicitThinkingLevel);
1393
+ }
1394
+ continue;
1395
+ }
1396
+
1340
1397
  const { model, thinkingLevel, warning, explicitThinkingLevel } = parseModelPatternWithContext(
1341
1398
  pattern,
1342
1399
  availableModels,
@@ -1386,7 +1443,7 @@ export async function resolveAllowedModels(
1386
1443
  if (!patterns || patterns.length === 0) {
1387
1444
  return available;
1388
1445
  }
1389
- const scoped = await resolveModelScope(patterns, modelRegistry, preferences);
1446
+ const scoped = await resolveModelScope(patterns, modelRegistry, preferences, settings);
1390
1447
  if (scoped.length === 0) {
1391
1448
  return [];
1392
1449
  }
@@ -1415,6 +1472,7 @@ export async function resolveAllowedModels(
1415
1472
  export function filterAvailableModelsByEnabledPatterns(
1416
1473
  available: Model<Api>[],
1417
1474
  patterns: readonly string[],
1475
+ settings?: Settings,
1418
1476
  ): Model<Api>[] {
1419
1477
  if (patterns.length === 0) return available;
1420
1478
 
@@ -1432,6 +1490,13 @@ export function filterAvailableModelsByEnabledPatterns(
1432
1490
  continue;
1433
1491
  }
1434
1492
 
1493
+ // Mirror resolveModelScope: role aliases resolve to the role's model.
1494
+ if (settings && modelRoleAliasPrefixLength(pattern) !== undefined) {
1495
+ const { model } = resolveModelRoleValue(pattern, available, { settings });
1496
+ if (model) addAllowed(model);
1497
+ continue;
1498
+ }
1499
+
1435
1500
  const { model } = parseModelPatternWithContext(pattern, available, context);
1436
1501
  if (model) {
1437
1502
  addAllowed(model);
@@ -1456,9 +1521,10 @@ export function resolveCliModel(options: {
1456
1521
  cliProvider?: string;
1457
1522
  cliModel?: string;
1458
1523
  modelRegistry: CliModelRegistry;
1524
+ settings?: Settings;
1459
1525
  preferences?: ModelMatchPreferences;
1460
1526
  }): ResolveCliModelResult {
1461
- const { cliProvider, cliModel, modelRegistry, preferences } = options;
1527
+ const { cliProvider, cliModel, modelRegistry, settings, preferences } = options;
1462
1528
 
1463
1529
  if (!cliModel) {
1464
1530
  return { model: undefined, selector: undefined, warning: undefined, error: undefined };
@@ -1474,6 +1540,19 @@ export function resolveCliModel(options: {
1474
1540
  };
1475
1541
  }
1476
1542
 
1543
+ if (!cliProvider && modelRoleAliasPrefixLength(cliModel) !== undefined) {
1544
+ const resolved = resolveModelRoleValue(cliModel, availableModels, { settings, matchPreferences: preferences });
1545
+ if (resolved.model) {
1546
+ return {
1547
+ model: resolved.model,
1548
+ selector: formatModelString(resolved.model),
1549
+ thinkingLevel: resolved.thinkingLevel,
1550
+ warning: resolved.warning,
1551
+ error: undefined,
1552
+ };
1553
+ }
1554
+ }
1555
+
1477
1556
  const providerMap = new Map<string, string>();
1478
1557
  for (const model of availableModels) {
1479
1558
  providerMap.set(model.provider.toLowerCase(), model.provider);
@@ -5,6 +5,20 @@
5
5
  import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
6
6
  import type { Settings } from "./settings";
7
7
 
8
+ /** Canonical prefix for a configured model role selector. */
9
+ export const MODEL_ROLE_ALIAS_PREFIX = "@";
10
+
11
+ /** Legacy prefix accepted for backwards-compatible role selectors. */
12
+ export const LEGACY_MODEL_ROLE_ALIAS_PREFIX = "pi/";
13
+
14
+ /** Shorthand selector for the default model role. */
15
+ export const DEFAULT_MODEL_ROLE_ALIAS = "*";
16
+
17
+ /** Format a model role as its canonical selector. */
18
+ export function formatModelRoleAlias(role: string): string {
19
+ return `${MODEL_ROLE_ALIAS_PREFIX}${role}`;
20
+ }
21
+
8
22
  export type ModelRole =
9
23
  | "default"
10
24
  | "smol"
@@ -117,7 +117,7 @@ export const TAB_METADATA: Record<SettingTab, { label: string; icon: `tab.${stri
117
117
  */
118
118
  export const TAB_GROUPS: Record<SettingTab, readonly string[]> = {
119
119
  appearance: ["Theme", "Status Line", "Display", "Images"],
120
- model: ["Thinking", "Sampling", "Prompt", "Retry & Fallback", "Advisor", "Vision"],
120
+ model: ["Thinking", "Sampling", "Prompt", "Retry & Fallback", "Advisor", "Prewalk", "Vision"],
121
121
  interaction: [
122
122
  "Input",
123
123
  "Approvals",
@@ -331,6 +331,25 @@ export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
331
331
  tool: "write",
332
332
  message: "Use the `write` tool instead of echo/cat redirection. It handles encoding and provides confirmation.",
333
333
  },
334
+ {
335
+ pattern: "^\\s*nohup\\s+|(?<!&)\\&\\s*$",
336
+ tool: "launch",
337
+ message:
338
+ "Use the `launch` tool instead of nohup or background shell syntax so the process stays observable and managed.",
339
+ },
340
+ {
341
+ pattern:
342
+ "^\\s*(?:(?:bun|npm|pnpm|yarn)\\s+(?:run\\s+)?(?:dev|start)(?:\\s|$)|(?:vite|next\\s+dev|nuxt\\s+dev|nodemon|lldb|gdb|tail\\s+-f)(?:\\s|$)|docker\\s+compose\\s+up(?!.*(?:\\s-d(?:\\s|$)|--detach))(?:\\s|$))",
343
+ tool: "launch",
344
+ message:
345
+ "Use the `launch` tool for services, watchers, and debuggers so other omp instances can observe and control them.",
346
+ },
347
+ {
348
+ pattern:
349
+ "^\\s*(?:(?:bun|npm|pnpm|yarn)\\s+(?:run\\s+)?\\S+|cargo\\s+watch|watchexec|pytest|vitest|jest|tsc)(?:.|\\n)*(?:--watch|-w)(?:\\s|$)",
350
+ tool: "launch",
351
+ message: "Use the `launch` tool for watch mode so its output, input, and lifecycle stay managed.",
352
+ },
334
353
  ];
335
354
 
336
355
  export const SETTINGS_SCHEMA = {
@@ -403,6 +422,17 @@ export const SETTINGS_SCHEMA = {
403
422
  "Pair a second model (assigned to the 'advisor' role) that passively reviews each turn and injects notes.",
404
423
  },
405
424
  },
425
+ "prewalk.enabled": {
426
+ type: "boolean",
427
+ default: false,
428
+ ui: {
429
+ tab: "model",
430
+ group: "Prewalk",
431
+ label: "Enable Prewalk",
432
+ description:
433
+ "Start on the active model, then switch to a fast/cheap model (default the 'smol' role) at the first edit/write after the plan nudge's todo list exists — the strong model plans, commits the todos, and starts the implementation before handing off. Overridable per session with --prewalk / --no-prewalk.",
434
+ },
435
+ },
406
436
  "advisor.subagents": {
407
437
  type: "boolean",
408
438
  default: false,
@@ -867,6 +897,17 @@ export const SETTINGS_SCHEMA = {
867
897
  description: "Remove the 1-character horizontal padding from the left and right of the terminal output",
868
898
  },
869
899
  },
900
+ "tui.scrollbackRebuild": {
901
+ type: "boolean",
902
+ default: false,
903
+ ui: {
904
+ tab: "appearance",
905
+ group: "Display",
906
+ label: "Rewrite Scrollback",
907
+ description:
908
+ "Erase and replay terminal scrollback when a block's final form replaces its live preview. When off (default), stale preview copies remain in history and the final content is appended below.",
909
+ },
910
+ },
870
911
 
871
912
  "display.shimmer": {
872
913
  type: "enum",
@@ -918,6 +959,18 @@ export const SETTINGS_SCHEMA = {
918
959
  },
919
960
  },
920
961
 
962
+ "display.collapseCompacted": {
963
+ type: "boolean",
964
+ default: true,
965
+ ui: {
966
+ tab: "appearance",
967
+ group: "Display",
968
+ label: "Collapse Compacted History",
969
+ description:
970
+ "Collapse pre-compaction history behind the summary divider on the live transcript; disable to keep the full transcript inline with dividers at each compaction point",
971
+ },
972
+ },
973
+
921
974
  showHardwareCursor: {
922
975
  type: "boolean",
923
976
  default: true, // will be computed based on platform if undefined
@@ -1239,7 +1292,7 @@ export const SETTINGS_SCHEMA = {
1239
1292
  textVerbosity: {
1240
1293
  type: "enum",
1241
1294
  values: ["low", "medium", "high"] as const,
1242
- default: "high",
1295
+ default: "medium",
1243
1296
  ui: {
1244
1297
  tab: "model",
1245
1298
  group: "Sampling",
@@ -1247,8 +1300,8 @@ export const SETTINGS_SCHEMA = {
1247
1300
  description: "OpenAI Responses and Codex response verbosity (low, medium, or high)",
1248
1301
  options: [
1249
1302
  { value: "low", label: "Low", description: "Prefer concise responses" },
1250
- { value: "medium", label: "Medium", description: "Balance brevity and detail" },
1251
- { value: "high", label: "High", description: "Prefer detailed responses (default)" },
1303
+ { value: "medium", label: "Medium", description: "Balance brevity and detail (default)" },
1304
+ { value: "high", label: "High", description: "Prefer detailed responses" },
1252
1305
  ],
1253
1306
  },
1254
1307
  },
@@ -2597,14 +2650,14 @@ export const SETTINGS_SCHEMA = {
2597
2650
  group: "Mnemopi",
2598
2651
  label: "Mnemopi LLM Mode",
2599
2652
  description:
2600
- "Use no LLM, the online tiny model (the TINY role from /models, else pi/smol), or a remote OpenAI-compatible endpoint",
2653
+ "Use no LLM, the online tiny model (the TINY role from /models, else @smol), or a remote OpenAI-compatible endpoint",
2601
2654
  condition: "mnemopiActive",
2602
2655
  options: [
2603
2656
  { value: "none", label: "None", description: "Disable Mnemopi LLM-backed extraction" },
2604
2657
  {
2605
2658
  value: "smol",
2606
2659
  label: "Online (tiny)",
2607
- description: "Use the online tiny model (the TINY role from /models, else pi/smol)",
2660
+ description: "Use the online tiny model (the TINY role from /models, else @smol)",
2608
2661
  },
2609
2662
  { value: "remote", label: "Remote", description: "Use the Mnemopi remote LLM settings below" },
2610
2663
  ],
@@ -3518,6 +3571,17 @@ export const SETTINGS_SCHEMA = {
3518
3571
  },
3519
3572
  },
3520
3573
 
3574
+ "launch.enabled": {
3575
+ type: "boolean",
3576
+ default: true,
3577
+ ui: {
3578
+ tab: "tools",
3579
+ group: "Available Tools",
3580
+ label: "Launch",
3581
+ description: "Enable the launch tool for supervising shared long-running project processes",
3582
+ },
3583
+ },
3584
+
3521
3585
  "speechgen.enabled": {
3522
3586
  type: "boolean",
3523
3587
  default: false,
@@ -4547,7 +4611,7 @@ export const SETTINGS_SCHEMA = {
4547
4611
  group: "Tiny Model",
4548
4612
  label: "Tiny Model",
4549
4613
  description:
4550
- "Session-title model: online (the TINY role from /models, else pi/smol) by default, or a local on-device model",
4614
+ "Session-title model: online (the TINY role from /models, else @smol) by default, or a local on-device model",
4551
4615
  options: TINY_TITLE_MODEL_OPTIONS,
4552
4616
  },
4553
4617
  },
@@ -423,22 +423,25 @@ function formatStreamingDiff(
423
423
  cache?: RenderedStringCache,
424
424
  ): string {
425
425
  if (!diff) return "";
426
- // Clamp the collapsed tail to the viewport so a tall or fast-growing diff
427
- // cannot outgrow the live window. Otherwise its mutating tail scrolls above
428
- // the native-scrollback commit boundary and the engine re-commits a fresh
429
- // snapshot every streamed frame, stacking duplicate "… more lines above"
430
- // previews in history. The budget is VISUAL rows (a long wrapped line counts
426
+ // Clamp the tail to the viewport so a tall or fast-growing diff cannot
427
+ // outgrow the live window. Otherwise its mutating rows scroll above the
428
+ // native-scrollback commit boundary mid-stream and freeze into immutable
429
+ // history as a stale preview snapshot; the finalize repair then recommits
430
+ // the final render below it a duplicated block on the tape. Collapsed
431
+ // gets a short fixed tail; expanded widens it to the viewport-sized window,
432
+ // never unbounded. The budget is VISUAL rows (a long wrapped line counts
431
433
  // for more than one) at the framed block's inner width (border only —
432
- // contentPaddingLeft is 0); only the visible suffix is syntax-colored, so the
433
- // cheap raw-line wrap walk keeps the per-chunk cost bounded. innerWidth/budget
434
- // are in the cache salt so a resize re-slices.
434
+ // contentPaddingLeft is 0); only the visible suffix is syntax-colored, so
435
+ // the cheap raw-line wrap walk keeps the per-chunk cost bounded.
436
+ // innerWidth/budget are in the cache salt so a resize re-slices.
435
437
  const innerWidth = Math.max(1, width - 2);
436
- const budget = expanded ? Number.POSITIVE_INFINITY : Math.min(EDIT_STREAMING_PREVIEW_LINES, previewWindowRows());
438
+ const budget = expanded ? previewWindowRows() : Math.min(EDIT_STREAMING_PREVIEW_LINES, previewWindowRows());
437
439
  let text = cachedRenderedString(cache, uiTheme, expanded, `${rawPath}:${innerWidth}:${budget}`, diff, () => {
438
440
  // "Cursor" tail window: pin the last rows to the bottom so freshly streamed
439
441
  // changes stay on screen. The whole-file diff is recomputed every chunk and
440
442
  // its Myers alignment is not monotonic in payload length, so a hunk-aware
441
- // window stutters as rows move between hunks. Expanded lifts the cap.
443
+ // window stutters as rows move between hunks. Expanded widens the window
444
+ // to the viewport; the full diff appears once the result finalizes.
442
445
  const allLines = diff.replace(/\n+$/u, "").split("\n");
443
446
  let visualUsed = 0;
444
447
  let cut = allLines.length;