@oh-my-pi/pi-coding-agent 16.1.9 → 16.1.11

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 (81) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/cli.js +3455 -3430
  3. package/dist/types/config/settings-schema.d.ts +10 -0
  4. package/dist/types/edit/streaming.d.ts +5 -5
  5. package/dist/types/export/html/index.d.ts +31 -2
  6. package/dist/types/export/html/web-palette.d.ts +117 -0
  7. package/dist/types/hindsight/content.d.ts +7 -0
  8. package/dist/types/hindsight/transcript.d.ts +1 -1
  9. package/dist/types/modes/components/hook-editor.d.ts +2 -1
  10. package/dist/types/modes/controllers/selector-controller.d.ts +0 -1
  11. package/dist/types/modes/interactive-mode.d.ts +5 -0
  12. package/dist/types/modes/types.d.ts +17 -0
  13. package/dist/types/modes/utils/keybinding-matchers.d.ts +13 -0
  14. package/dist/types/sdk.d.ts +1 -0
  15. package/dist/types/session/agent-session.d.ts +4 -0
  16. package/dist/types/session/session-context.d.ts +2 -0
  17. package/dist/types/session/session-entries.d.ts +6 -0
  18. package/dist/types/session/session-manager.d.ts +2 -1
  19. package/dist/types/system-prompt.d.ts +2 -0
  20. package/dist/types/tools/acp-bridge.d.ts +29 -0
  21. package/dist/types/tools/browser/aria/aria-snapshot.d.ts +39 -0
  22. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +10 -0
  23. package/dist/types/tools/browser/tab-worker.d.ts +20 -0
  24. package/dist/types/tools/browser.d.ts +1 -0
  25. package/dist/types/tools/find.d.ts +1 -2
  26. package/dist/types/tools/write.d.ts +1 -2
  27. package/dist/types/utils/image-loading.d.ts +4 -3
  28. package/dist/types/utils/jj.d.ts +25 -0
  29. package/dist/types/utils/title-generator.d.ts +0 -2
  30. package/package.json +12 -12
  31. package/scripts/generate-aria-snapshot.ts +134 -0
  32. package/scripts/generate-share-viewer.ts +4 -2
  33. package/src/autoresearch/git.ts +12 -0
  34. package/src/config/settings-schema.ts +12 -0
  35. package/src/discovery/opencode.ts +47 -4
  36. package/src/edit/hashline/filesystem.ts +8 -0
  37. package/src/edit/modes/patch.ts +18 -2
  38. package/src/edit/modes/replace.ts +13 -10
  39. package/src/edit/streaming.ts +5 -5
  40. package/src/export/html/index.ts +50 -8
  41. package/src/export/html/web-palette.ts +142 -0
  42. package/src/hindsight/backend.ts +4 -4
  43. package/src/hindsight/content.ts +17 -1
  44. package/src/hindsight/transcript.ts +2 -2
  45. package/src/internal-urls/docs-index.generated.txt +1 -1
  46. package/src/modes/components/agent-dashboard.ts +8 -8
  47. package/src/modes/components/hook-editor.ts +13 -10
  48. package/src/modes/components/session-selector.ts +3 -0
  49. package/src/modes/controllers/event-controller.ts +29 -5
  50. package/src/modes/controllers/extension-ui-controller.ts +6 -2
  51. package/src/modes/controllers/selector-controller.ts +3 -5
  52. package/src/modes/interactive-mode.ts +69 -29
  53. package/src/modes/theme/dark.json +1 -1
  54. package/src/modes/types.ts +18 -0
  55. package/src/modes/utils/keybinding-matchers.ts +36 -1
  56. package/src/modes/utils/ui-helpers.ts +1 -0
  57. package/src/prompts/system/project-prompt.md +2 -0
  58. package/src/prompts/system/system-prompt.md +0 -1
  59. package/src/prompts/tools/browser.md +5 -2
  60. package/src/sdk.ts +20 -2
  61. package/src/session/agent-session.ts +38 -14
  62. package/src/session/session-context.ts +5 -0
  63. package/src/session/session-entries.ts +6 -0
  64. package/src/session/session-manager.ts +3 -1
  65. package/src/system-prompt.ts +15 -3
  66. package/src/task/worktree.ts +12 -4
  67. package/src/thinking.ts +1 -1
  68. package/src/tools/acp-bridge.ts +66 -0
  69. package/src/tools/bash.ts +3 -3
  70. package/src/tools/browser/aria/aria-snapshot.bundle.txt +7 -0
  71. package/src/tools/browser/aria/aria-snapshot.ts +103 -0
  72. package/src/tools/browser/cmux/cmux-tab.ts +73 -1
  73. package/src/tools/browser/tab-worker.ts +242 -47
  74. package/src/tools/browser.ts +5 -0
  75. package/src/tools/find.ts +1 -2
  76. package/src/tools/index.ts +1 -1
  77. package/src/tools/write.ts +3 -27
  78. package/src/utils/git.ts +3 -2
  79. package/src/utils/image-loading.ts +6 -3
  80. package/src/utils/jj.ts +47 -0
  81. package/src/utils/title-generator.ts +31 -99
@@ -9,9 +9,10 @@ export const SUPPORTED_INPUT_IMAGE_MIME_TYPES = SUPPORTED_IMAGE_MIME_TYPES;
9
9
 
10
10
  /**
11
11
  * Ollama and its local-backend family decode image input through llama.cpp /
12
- * `stb_image`, which is compiled without WebP support, so a WebP upload fails
13
- * with an opaque HTTP 400. Detect those models so the resize pipeline encodes
14
- * to PNG/JPEG instead — the automatic equivalent of `OMP_NO_WEBP=1`.
12
+ * `stb_image`, which is compiled without WebP support. The first-party Codex
13
+ * Responses backend also rejects WebP `input_image.image_url` data URLs. Detect
14
+ * those models so the resize pipeline encodes to PNG/JPEG instead — the
15
+ * automatic equivalent of `OMP_NO_WEBP=1`.
15
16
  */
16
17
  export function modelLacksWebpSupport(
17
18
  model: Pick<Model, "provider" | "api" | "imageInputDecoder"> | undefined,
@@ -19,6 +20,8 @@ export function modelLacksWebpSupport(
19
20
  if (!model) return false;
20
21
  return (
21
22
  model.imageInputDecoder === "stb" ||
23
+ model.api === "openai-codex-responses" ||
24
+ model.provider === "openai-codex" ||
22
25
  model.provider === "ollama" ||
23
26
  model.provider === "ollama-cloud" ||
24
27
  model.provider === "llama.cpp" ||
package/src/utils/jj.ts CHANGED
@@ -2,6 +2,7 @@ import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { $which } from "@oh-my-pi/pi-utils";
4
4
  import { LRUCache } from "lru-cache/raw";
5
+ import * as git from "./git";
5
6
 
6
7
  // ════════════════════════════════════════════════════════════════════════════
7
8
  // Types
@@ -246,3 +247,49 @@ export const repo = {
246
247
  return (await repo.root(cwd)) !== null;
247
248
  },
248
249
  };
250
+
251
+ /**
252
+ * Detect a "pure" Jujutsu workspace — one where Git-mutating automation has
253
+ * no safe Git target. Invoking `git checkout -b`, `git worktree add`, or
254
+ * `git apply` against a pure jj workspace either fails outright (no `.git/`
255
+ * present) or mutates state that jj itself cannot reconcile.
256
+ *
257
+ * `cwd` is "pure jj" iff its nearest jj workspace ancestor is **closer than**
258
+ * its nearest Git checkout ancestor (or no Git checkout is present at all).
259
+ * Both lookups walk upward from `cwd`, so the deeper ancestor is the one the
260
+ * user is actually working inside.
261
+ *
262
+ * Returns:
263
+ * - `false` for plain Git checkouts (no jj metadata anywhere up the tree).
264
+ * - `false` for colocated jj-git workspaces — `jj git init --colocate` keeps
265
+ * `.jj/` and `.git/` at the same root.
266
+ * - `false` when a nested Git checkout (e.g. a vendored repo or fixture)
267
+ * lives **under** an outer jj workspace; Git automation targets the inner
268
+ * repo and never touches the surrounding jj tree.
269
+ * - `true` when jj is the deeper ancestor — either a standalone pure jj
270
+ * workspace, or a jj workspace nested under an unrelated outer Git
271
+ * checkout, where Git automation against the outer root would silently
272
+ * bypass jj.
273
+ * - `false` for directories backed by neither tool.
274
+ */
275
+ export async function isPureJjRepo(cwd: string): Promise<boolean> {
276
+ const jjRoot = await repo.root(cwd);
277
+ if (jjRoot === null) return false;
278
+ const gitRoot = await git.repo.root(cwd);
279
+ if (gitRoot === null) return true;
280
+ return isStrictDescendant(path.resolve(jjRoot), path.resolve(gitRoot));
281
+ }
282
+
283
+ /**
284
+ * Return `true` when `child` is a strict descendant of `ancestor` (same path
285
+ * counts as `false`). Both arguments must already be resolved absolute paths.
286
+ */
287
+ function isStrictDescendant(child: string, ancestor: string): boolean {
288
+ const rel = path.relative(ancestor, child);
289
+ if (rel === "" || rel === ".") return false;
290
+ if (rel.startsWith("..")) return false;
291
+ // `path.relative` returns an absolute path only when the two arguments
292
+ // live on different filesystem roots (Windows drives, UNC shares); not a
293
+ // real ancestor relationship.
294
+ return !path.isAbsolute(rel);
295
+ }
@@ -12,7 +12,7 @@ import type { Settings } from "../config/settings";
12
12
  import titleMarkerInstruction from "../prompts/system/title-marker-instruction.md" with { type: "text" };
13
13
  import titleSystemPrompt from "../prompts/system/title-system.md" with { type: "text" };
14
14
  import titleMarkerSystemPrompt from "../prompts/system/title-system-marker.md" with { type: "text" };
15
- import { ONLINE_TINY_TITLE_MODEL_KEY } from "../tiny/models";
15
+ import { isTinyTitleLocalModelKey, ONLINE_TINY_TITLE_MODEL_KEY } from "../tiny/models";
16
16
  import { formatTitleUserMessage, isLowSignalTitleInput, normalizeGeneratedTitle } from "../tiny/text";
17
17
  import { tinyTitleClient } from "../tiny/title-client";
18
18
 
@@ -23,7 +23,6 @@ const TITLE_MARKER_INSTRUCTION = prompt.render(titleMarkerInstruction);
23
23
  const DEFAULT_TERMINAL_TITLE = "π";
24
24
  const TERMINAL_TITLE_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
25
25
 
26
- export const TITLE_LOCAL_FALLBACK_DELAY_MS = 10_000;
27
26
  const TITLE_MAX_TOKENS = 30;
28
27
  const REASONING_SAFE_MAX_TOKENS = 1024;
29
28
  const SET_TITLE_TOOL_NAME = "set_title";
@@ -81,78 +80,6 @@ function getTitleModel(registry: ModelRegistry, settings: Settings, currentModel
81
80
  return undefined;
82
81
  }
83
82
 
84
- export async function raceFirstNonNull<T>(
85
- primary: Promise<T | null>,
86
- startFallback: () => Promise<T | null>,
87
- delayMs: number = TITLE_LOCAL_FALLBACK_DELAY_MS,
88
- onPrimaryWinAfterFallback?: () => void,
89
- ): Promise<T | null> {
90
- const { promise, resolve } = Promise.withResolvers<T | null>();
91
- let resolved = false;
92
- let primarySettled = false;
93
- let fallbackStarted = false;
94
- let fallbackSettled = false;
95
-
96
- const resolveOnce = (value: T | null): void => {
97
- if (resolved) return;
98
- resolved = true;
99
- resolve(value);
100
- };
101
- const maybeResolveNull = (): void => {
102
- if (primarySettled && fallbackStarted && fallbackSettled) resolveOnce(null);
103
- };
104
- const startFallbackOnce = (): void => {
105
- if (fallbackStarted || resolved) return;
106
- fallbackStarted = true;
107
- let fallback: Promise<T | null>;
108
- try {
109
- fallback = startFallback();
110
- } catch {
111
- fallbackSettled = true;
112
- maybeResolveNull();
113
- return;
114
- }
115
- void fallback.then(
116
- value => {
117
- fallbackSettled = true;
118
- if (value !== null) resolveOnce(value);
119
- else maybeResolveNull();
120
- },
121
- () => {
122
- fallbackSettled = true;
123
- maybeResolveNull();
124
- },
125
- );
126
- };
127
-
128
- const timer = setTimeout(startFallbackOnce, delayMs);
129
- void primary.then(
130
- value => {
131
- primarySettled = true;
132
- clearTimeout(timer);
133
- if (value !== null) {
134
- if (fallbackStarted) onPrimaryWinAfterFallback?.();
135
- resolveOnce(value);
136
- return;
137
- }
138
- startFallbackOnce();
139
- maybeResolveNull();
140
- },
141
- () => {
142
- primarySettled = true;
143
- clearTimeout(timer);
144
- startFallbackOnce();
145
- maybeResolveNull();
146
- },
147
- );
148
-
149
- try {
150
- return await promise;
151
- } finally {
152
- clearTimeout(timer);
153
- }
154
- }
155
-
156
83
  /**
157
84
  * Generate a title for a session based on the first user message.
158
85
  *
@@ -200,36 +127,41 @@ export async function generateSessionTitle(
200
127
  );
201
128
  }
202
129
 
203
- const onlineAbortController = new AbortController();
204
- const localTitlePromise = titleSystemPrompt
205
- ? tinyTitleClient.generate(tinyModel, firstMessage, { systemPrompt: titleSystemPrompt })
206
- : tinyTitleClient.generate(tinyModel, firstMessage);
207
- const localTitle = localTitlePromise.then(
208
- title => title || null,
209
- err => {
210
- logger.warn("title-generator: local model error", {
130
+ // User explicitly picked a local tiny model. NEVER fall back to the online
131
+ // smol path (issue #3187): the smol role resolves through priority.json and
132
+ // silently bills whatever provider holds the resolved API key — OpenRouter
133
+ // in the reporter's case, leaking real credits without consent. If the
134
+ // local worker fails (unknown key, download missing, transformers.js
135
+ // crash, abort), leave the session untitled; the next user turn retries.
136
+ if (!isTinyTitleLocalModelKey(tinyModel)) {
137
+ logger.warn("title-generator: unknown local tiny model; skipping title (will not fall back to online)", {
138
+ sessionId,
139
+ model: tinyModel,
140
+ reason: "unknown-local-model",
141
+ });
142
+ return null;
143
+ }
144
+ try {
145
+ const localTitle = titleSystemPrompt
146
+ ? await tinyTitleClient.generate(tinyModel, firstMessage, { systemPrompt: titleSystemPrompt })
147
+ : await tinyTitleClient.generate(tinyModel, firstMessage);
148
+ if (!localTitle) {
149
+ logger.warn("title-generator: local tiny model produced no title; skipping (no online fallback)", {
211
150
  sessionId,
212
151
  model: tinyModel,
213
- error: err instanceof Error ? err.message : String(err),
152
+ reason: "local-no-output",
214
153
  });
215
154
  return null;
216
- },
217
- );
218
- const startOnline = (): Promise<string | null> =>
219
- generateTitleOnline(
220
- firstMessage,
221
- registry,
222
- settings,
155
+ }
156
+ return localTitle;
157
+ } catch (err) {
158
+ logger.warn("title-generator: local tiny model errored; skipping (no online fallback)", {
223
159
  sessionId,
224
- currentModel,
225
- metadataResolver,
226
- onlineAbortController.signal,
227
- titleSystemPrompt,
228
- );
229
-
230
- return raceFirstNonNull(localTitle, startOnline, TITLE_LOCAL_FALLBACK_DELAY_MS, () => {
231
- onlineAbortController.abort();
232
- });
160
+ model: tinyModel,
161
+ error: err instanceof Error ? err.message : String(err),
162
+ });
163
+ return null;
164
+ }
233
165
  }
234
166
 
235
167
  export async function generateTitleOnline(