pi2dsh 0.12.4 → 0.13.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.
package/README.md CHANGED
@@ -170,6 +170,7 @@ it work. **This is the list to trust.**
170
170
  | Plugin | What was exercised | Where | Example |
171
171
  |---|---|---|---|
172
172
  | [`@kassing/pi-vision`](https://www.npmjs.com/package/@kassing/pi-vision) | Image analysis delegated to a vision model; image-admission companion route; analysis injected into a text-only model's turn | CLI + web | [`vision-bridge`](examples/vision-bridge/) |
173
+ | [`@crazygit/pi-codex-image-gen`](https://www.npmjs.com/package/@crazygit/pi-codex-image-gen) | ChatGPT/Codex OAuth → `gpt-image-2` generation; local reference-image upload through DSH approval; image edit; native attachment storage and inline Web rendering | CLI + web | [`codex-image-gen`](examples/codex-image-gen/) |
173
174
  | [`pi-btw`](https://www.npmjs.com/package/pi-btw) | `/btw <question>` as a real child session in DSH's subagent UI; `/btw-inject`; `/btw --save`; main thread stays clean | CLI + web | [`side-conversation`](examples/side-conversation/) |
174
175
  | [`pi-powerline-footer`](https://www.npmjs.com/package/pi-powerline-footer) | A terminal status line — model, thinking level, project, context usage — drawn into DSH's widget dock, colour included | web | [`presentation-surfaces`](examples/presentation-surfaces/) |
175
176
  | [`pi-vision-tool`](https://www.npmjs.com/package/pi-vision-tool) | Tool registration through a JSON-Schema shape DSH had to convert (`anyOf` → `oneOf`) | CLI + web | — |
@@ -306,23 +307,71 @@ The rules that keep it honest:
306
307
  - **Verified, not asserted.** Every capability has a public-API contract test,
307
308
  and ships only after running end to end on a real DSH loop — CLI *and* web.
308
309
 
310
+ ## What this is teaching us about DSH
311
+
312
+ pi2dsh is also an executable stress test of DSH's plugin architecture. Pi gives
313
+ that test a useful workload: a large, already-used public plugin ABI rather than
314
+ a set of examples invented to fit the host.
315
+
316
+ The result so far is specific, not a thumbs-up/thumbs-down verdict:
317
+
318
+ - DSH's public seams successfully carry whole capabilities: tools, commands,
319
+ model adapters, user questions, native child sessions and browser slots.
320
+ - The pressure points appear when an out-of-repo plugin needs to extend an
321
+ existing capability *from the inside*: add a durable session-event type,
322
+ intercept the real provider request/response, control compaction before it
323
+ happens, or participate in trust before project resources load.
324
+ - A working pi2dsh sidecar or alternate adapter is useful product behaviour,
325
+ but it is **not** counted as proof that the native DSH seam is complete.
326
+
327
+ For example, a `pi-btw` answer is a real DSH child session — visible, resumable
328
+ and continuable by the host. Pi custom entries are different: they still need a
329
+ pi2dsh sidecar because an out-of-repo plugin cannot safely add a new event type
330
+ to DSH's durable log. Likewise, a transport-owning Pi provider can register a
331
+ native DSH route with its full compatibility profile, while the hand-configured
332
+ `llm-pi-ai` schema still drops some wire-compatibility fields.
333
+
334
+ The project follows one **[Pi → DSH architecture mapping standard](docs/architecture-mapping-standard.md)**.
335
+ All 111 Pi surfaces belong to capability contracts, all 45 DSH subsystems belong
336
+ to carrying mechanisms, and each capability contract maps to concrete public
337
+ DSH seams. Real plugin runs reference those mappings and record the five-layer
338
+ trace and five-grade result. The structured source of truth is
339
+ [`architecture-ledger.json`](docs/architecture-ledger.json); it produces the
340
+ **[theoretical architecture matrix](docs/architecture-mapping-matrix.md)**,
341
+ **[real-plugin validation matrix](docs/plugin-validation-matrix.md)** and
342
+ **[current architecture conclusions](docs/dsh-architecture-conformance.md)**.
343
+ That makes both top-down status and per-plugin drill-down answerable from the
344
+ same ledger. The current five DSH gap IDs are confirmed findings, not a claim
345
+ of complete coverage. Current
346
+ upstream reports include
347
+ [#2708 — durable events for out-of-repo plugins](https://github.com/deepseek-ai/deepseek-harness/discussions/2708)
348
+ and
349
+ [#3076 — provider compat fields dropped by `llm-pi-ai`](https://github.com/deepseek-ai/deepseek-harness/discussions/3076).
350
+
309
351
  ## Pi capabilities on DSH
310
352
 
311
353
  Every surface a Pi package can touch, and what it maps onto. These tables are
312
354
  generated from the rules the bridge consults at runtime, so they cannot drift
313
355
  from the code.
314
356
 
357
+ For pinned Pi 0.84.1, the generated catalog has **111 upstream-shaped rule
358
+ rows**. The bridge also keeps one
359
+ documented compatibility extension, `unregisterTool`; it appears on the tools
360
+ detail page but is deliberately outside that total. Nested objects such as the
361
+ session manager can expose several methods behind one row; the architecture
362
+ audit states this boundary explicitly.
363
+
315
364
  <!-- capability-table:start -->
316
365
  | Area | Pi surfaces | Status |
317
366
  |---|---|---|
318
- | [Tools](docs/capabilities/tools.md) | 12 | 3 same semantics · 9 mapped, difference stated |
319
- | [Commands, flags, editor input](docs/capabilities/commands.md) | 13 | 13 mapped, difference stated |
367
+ | [Tools](docs/capabilities/tools.md) | 11 | 2 same semantics · 9 mapped, difference stated |
368
+ | [Commands, flags, editor input](docs/capabilities/commands.md) | 6 | 6 mapped, difference stated |
320
369
  | [Messages, context, agent loop](docs/capabilities/conversation.md) | 20 | 9 same semantics · 11 mapped, difference stated |
321
- | [Sessions & side conversations](docs/capabilities/sessions.md) | 24 | 6 same semantics · 18 mapped, difference stated |
370
+ | [Sessions & side conversations](docs/capabilities/sessions.md) | 23 | 5 same semantics · 18 mapped, difference stated |
322
371
  | [Models, providers, credentials](docs/capabilities/models.md) | 15 | 1 same semantics · 11 mapped, difference stated · 3 not available |
323
- | [Asking the user, rendering](docs/capabilities/interaction.md) | 24 | 4 same semantics · 20 mapped, difference stated |
372
+ | [Asking the user, rendering](docs/capabilities/interaction.md) | 32 | 5 same semantics · 27 mapped, difference stated |
324
373
  | [Project environment & resources](docs/capabilities/environment.md) | 4 | 1 same semantics · 1 mapped, difference stated · 2 not available |
325
- | **Total** | **112** | **24 same semantics · 83 mapped, difference stated · 5 not available** |
374
+ | **Total** | **111** | **23 same semantics · 83 mapped, difference stated · 5 not available** |
326
375
  <!-- capability-table:end -->
327
376
 
328
377
  Plus **202 imported symbols** from Pi's three runtime packages
@@ -330,9 +379,10 @@ Plus **202 imported symbols** from Pi's three runtime packages
330
379
  shims — so a plugin's own Pi version pins never load. They are listed in
331
380
  [Imported Pi runtime symbols](docs/capabilities/imports.md).
332
381
 
333
- Each area page states two things for every surface: what it does on DSH, and
334
- **how it is built** the DSH seam, service or waterfall the mapping runs on,
335
- so a claim here can be checked against the harness instead of trusted.
382
+ Each area page states the Pi capability contract, the theoretical DSH mechanism
383
+ and public seam, and the current implementation for every surface. Adding a
384
+ surface without a capability assignment or theoretical mapping fails the docs
385
+ check.
336
386
 
337
387
  Start at the [capability index](docs/capabilities/README.md). Machine-readable:
338
388
  `pi2dsh matrix --json`.
@@ -369,8 +419,10 @@ in one has actually been executed against a real DSH loop before landing.
369
419
  | Example | What you get |
370
420
  |---|---|
371
421
  | [`vision-bridge`](examples/vision-bridge/) | A text-only model answers questions about images — CLI and web, probe images included |
422
+ | [`codex-image-gen`](examples/codex-image-gen/) | Generate and edit images with a ChatGPT/Codex subscription; DSH approval and inline result included |
372
423
  | [`side-conversation`](examples/side-conversation/) | `/btw <question>` runs a side thread in DSH's native subagent UI; your main conversation stays clean |
373
424
  | [`presentation-surfaces`](examples/presentation-surfaces/) | A real plugin's terminal chrome (`pi-powerline-footer`) in DSH's web seats, plus which of the top-50 Pi plugins draw at all |
425
+ | [`subscription-login`](examples/subscription-login/) | Use a ChatGPT / Claude / Copilot / Kimi subscription as a DSH model: `/login`, then the route and credential appear on their own |
374
426
  | [`gateway-compat`](examples/gateway-compat/) | Private / domestic / proxy gateways that reject the `developer` role: why reasoning 400s, and how a Pi provider plugin gets past it (recording proxy included, so you can read the exact request body we send) |
375
427
  | [`custom-gateways`](examples/custom-gateways/) | Add any OpenAI-compatible gateway the official DSH way, and every Pi plugin sees it |
376
428
 
@@ -391,6 +443,7 @@ pnpm verify # typecheck + contract tests + packaging
391
443
  pnpm audit:community # static screening over the top-50 corpus
392
444
  pnpm test:community # deep runtime + official plugin-manager + e2e
393
445
  DEEPSEEK_API_KEY=… pnpm test:live # real-model acceptance (key from env only)
446
+ CODEX_AUTH_FILE=… pnpm test:codex-image # real OAuth generation + reference edit + Web pixels
394
447
  ```
395
448
 
396
449
  Acceptance evidence per capability: [docs/acceptance.md](docs/acceptance.md).
package/README.zh.md CHANGED
@@ -152,6 +152,7 @@ Web 里**直接粘图**——哪怕你的主模型是纯文本的。DSH 正常
152
152
  | 插件 | 验证了什么 | 在哪验的 | 示例 |
153
153
  |---|---|---|---|
154
154
  | [`@kassing/pi-vision`](https://www.npmjs.com/package/@kassing/pi-vision) | 图片委托给视觉模型;贴图伴生路由;分析结果注入纯文本模型的这一轮 | CLI + Web | [`vision-bridge`](examples/vision-bridge/) |
155
+ | [`@crazygit/pi-codex-image-gen`](https://www.npmjs.com/package/@crazygit/pi-codex-image-gen) | ChatGPT/Codex OAuth 调 `gpt-image-2` 生图;本地参考图走 DSH 审批后上传;编辑图片;原生附件存储并在 Web 内直接显示 | CLI + Web | [`codex-image-gen`](examples/codex-image-gen/) |
155
156
  | [`pi-btw`](https://www.npmjs.com/package/pi-btw) | `/btw <问题>` 跑成 DSH 子代理界面里的真子会话;`/btw-inject`;`/btw --save`;主会话保持干净 | CLI + Web | [`side-conversation`](examples/side-conversation/) |
156
157
  | [`pi-powerline-footer`](https://www.npmjs.com/package/pi-powerline-footer) | 终端状态条(模型、思考档位、项目、上下文用量)画进 DSH 的 widget dock,带颜色 | Web | [`presentation-surfaces`](examples/presentation-surfaces/) |
157
158
  | [`pi-vision-tool`](https://www.npmjs.com/package/pi-vision-tool) | 工具注册,且带一个 DSH 需要转换的 JSON Schema 形状(`anyOf` → `oneOf`) | CLI + Web | — |
@@ -265,31 +266,70 @@ footer、title、working/thinking 类),都画在宿主自己的 slot 座位
265
266
  - **验证过才算数。** 每项能力都有公开 API 契约测试,并且必须在真实 DSH loop 上
266
267
  端到端跑通——CLI **和** Web 双端——才会发布。
267
268
 
269
+ ## 这件事正在检验 DSH 什么
270
+
271
+ pi2dsh 同时也是一套会执行的 DSH 插件架构压力测试。Pi 提供的不是为了迎合 DSH
272
+ 现编的几个 demo,而是一套已经被大量真实插件用过的公开 ABI,所以它很适合回答:
273
+ DSH 所说的“能力由插件自由组合”,到底走到了哪一步。
274
+
275
+ 目前的结论不是简单的“好”或“不好”,而是边界已经很清楚:
276
+
277
+ - DSH 对**替换一整项能力**的公开 seam 是成立的:工具、命令、模型 adapter、用户
278
+ 提问、原生子会话和浏览器 slot 都已经承接住真实 Pi 能力。
279
+ - 压力集中在**从内部扩展已有能力**:仓外插件新增一种持久会话事件、拦截真正发出的
280
+ provider 请求/响应、在压缩发生前改变决定、在项目资源加载前参与 trust。
281
+ - pi2dsh 用 sidecar 或另一条 adapter 把功能绕通,对用户有价值,但**不能算 DSH
282
+ 原生架构已经承接成功**。我们会把“功能可用”和“宿主 seam 完整”分开记。
283
+
284
+ 一个容易说错的例子:`pi-btw` 的回答已经是真正的 DSH child session,宿主能打开、
285
+ 续聊和恢复;sidecar 存的是 Pi 自定义 entry 等 DSH 仓外插件目前无法写进原生日志的
286
+ 事实,不是把整个子会话伪造了一遍。模型侧也一样:自带 transport 的 Pi provider
287
+ 可以注册成原生 DSH route,并保留完整 compat;但手写 `llm-pi-ai` 配置时,部分
288
+ wire 兼容字段仍会被 schema 丢掉。
289
+
290
+ 项目采用一套统一的 **[Pi → DSH 架构映射标准](docs/architecture-mapping-standard.md)**:
291
+ 111 条 Pi 接口先归入能力契约,DSH 的 45 个模块归入承载机制,再建立到具体公开 seam
292
+ 的理论映射;真实插件只引用这些映射,记录五层流转和五级结果。结构化事实保存在
293
+ [`architecture-ledger.json`](docs/architecture-ledger.json),由同一份总账生成
294
+ **[理论架构矩阵](docs/architecture-mapping-matrix.md)**、
295
+ **[真实插件验证矩阵](docs/plugin-validation-matrix.md)** 和
296
+ **[当前架构结论](docs/dsh-architecture-conformance.md)**。这样既能从总体看到已经适配的
297
+ 能力,也能从任一插件下钻到它用了哪些 Pi 能力、落到哪些 DSH 机制、实际达到哪一级。
298
+ 当前 5 个 DSH 缺口是已经坐实的发现,**不是已经完成全覆盖**。
299
+ 当前已向上游提交的实证包括
300
+ [#2708:让仓外插件安全写持久事件](https://github.com/deepseek-ai/deepseek-harness/discussions/2708)
301
+
302
+ [#3076:`llm-pi-ai` 丢 provider compat 字段](https://github.com/deepseek-ai/deepseek-harness/discussions/3076)。
303
+
268
304
  ## Pi 的开放能力在 DSH 上怎么落
269
305
 
270
306
  Pi 包能碰到的每一个面,以及它落到 DSH 的什么位置。下面这些表是从桥在运行时真正
271
307
  查的那份规则生成的,所以不会和代码脱节。
272
308
 
309
+ 针对固定的 Pi 0.84.1,当前生成目录有 **111** 条上游形状的规则行。桥另外保留了一个
310
+ 兼容扩展 `unregisterTool`;它会出现在工具细表中,但明确不计入这个总数。
311
+ `sessionManager` 等嵌套对象仍可能用一行代表多个方法,架构审计会明确写出这层边界。
312
+
273
313
  <!-- capability-table:start -->
274
314
  | 能力域 | Pi 面数 | 状态 |
275
315
  |---|---|---|
276
- | [工具](docs/capabilities/tools.md) | 12 | 3 语义一致 · 9 已映射并写明差异 |
277
- | [命令、flag、编辑器输入](docs/capabilities/commands.md) | 13 | 13 已映射并写明差异 |
316
+ | [工具](docs/capabilities/tools.md) | 11 | 2 语义一致 · 9 已映射并写明差异 |
317
+ | [命令、flag、编辑器输入](docs/capabilities/commands.md) | 6 | 6 已映射并写明差异 |
278
318
  | [消息、上下文、agent 循环](docs/capabilities/conversation.md) | 20 | 9 语义一致 · 11 已映射并写明差异 |
279
- | [会话与侧边对话](docs/capabilities/sessions.md) | 24 | 6 语义一致 · 18 已映射并写明差异 |
319
+ | [会话与侧边对话](docs/capabilities/sessions.md) | 23 | 5 语义一致 · 18 已映射并写明差异 |
280
320
  | [模型、provider、凭证](docs/capabilities/models.md) | 15 | 1 语义一致 · 11 已映射并写明差异 · 3 不提供 |
281
- | [向用户提问与渲染](docs/capabilities/interaction.md) | 24 | 4 语义一致 · 20 已映射并写明差异 |
321
+ | [向用户提问与渲染](docs/capabilities/interaction.md) | 32 | 5 语义一致 · 27 已映射并写明差异 |
282
322
  | [项目环境与资源](docs/capabilities/environment.md) | 4 | 1 语义一致 · 1 已映射并写明差异 · 2 不提供 |
283
- | **合计** | **112** | **24 语义一致 · 83 已映射并写明差异 · 5 不提供** |
323
+ | **合计** | **111** | **23 语义一致 · 83 已映射并写明差异 · 5 不提供** |
284
324
  <!-- capability-table:end -->
285
325
 
286
326
  另外还有 Pi 三个运行时包(`pi-coding-agent`、`pi-tui`、`pi-ai`)的 **202 个
287
327
  导入符号**,由 vendored 或 headless shim 提供——所以插件自己钉的 Pi 版本永远不会
288
328
  被加载,清单见[导入的 Pi 运行时符号](docs/capabilities/imports.md)。
289
329
 
290
- 每个能力域页面对每一个面都写两件事:它在 DSH 上做什么,以及**它是怎么实现的**
291
- ——这条映射落在哪个 DSH seam、服务或 waterfall 上,好让读者能对着 harness 核实,
292
- 而不是只能选择相信。
330
+ 每个能力域页面对每一个面都写清它属于哪个 Pi 能力契约、理论上应落到哪个 DSH 承载
331
+ 机制与公开 seam、当前代码实际上如何实现。新增接口如果没有能力归属或理论映射,文档
332
+ 检查会直接失败。
293
333
 
294
334
  从[能力索引](docs/capabilities/README.md)开始看。机器可读版:`pi2dsh matrix --json`。
295
335
 
@@ -318,8 +358,10 @@ loop 上实际跑过才会进来。
318
358
  | 示例 | 你能得到什么 |
319
359
  |---|---|
320
360
  | [`vision-bridge`](examples/vision-bridge/) | 纯文本模型回答图片问题——CLI 与 Web 双端,附探针图 |
361
+ | [`codex-image-gen`](examples/codex-image-gen/) | 用 ChatGPT/Codex 订阅生图和改图,包含 DSH 上传审批与 Web 内直接显示结果 |
321
362
  | [`side-conversation`](examples/side-conversation/) | `/btw <问题>` 在 DSH 原生子代理界面里开一条侧边线程,主会话保持干净 |
322
363
  | [`presentation-surfaces`](examples/presentation-surfaces/) | 真插件(`pi-powerline-footer`)的终端界面画进 DSH Web 座位,附 top50 里哪些 Pi 插件会画界面 |
364
+ | [`subscription-login`](examples/subscription-login/) | 用 ChatGPT / Claude / Copilot / Kimi 订阅账号当 DSH 的模型:`/login`、登录后自动建路由与凭证 |
323
365
  | [`gateway-compat`](examples/gateway-compat/) | 私有 / 国内 / 代理网关拒收 `developer` 角色:为什么一开推理就 400,以及用 Pi provider 插件怎么绕过去(附透传录制代理,能看到我们真正发出去的请求体) |
324
366
  | [`custom-gateways`](examples/custom-gateways/) | 按 DSH 官方方式接任何 OpenAI 兼容网关,每个 Pi 插件都能看到它 |
325
367
 
@@ -340,6 +382,7 @@ pnpm verify # 类型检查 + 契约测试 + 打包检查
340
382
  pnpm audit:community # 前 50 静态筛查
341
383
  pnpm test:community # 深度运行时 + 官方插件管理器 + e2e
342
384
  DEEPSEEK_API_KEY=… pnpm test:live # 真实模型验收(key 只从环境读)
385
+ CODEX_AUTH_FILE=… pnpm test:codex-image # 真 OAuth 生图 + 参考图编辑 + Web 像素显示
343
386
  ```
344
387
 
345
388
  逐项能力的验收证据:[docs/acceptance.md](docs/acceptance.md)。
package/dist/client.js CHANGED
@@ -316,8 +316,194 @@ window.__ModuleLoader__.load({
316
316
  flexDirection: "column",
317
317
  gap: "4px",
318
318
  padding: "4px 2px"
319
+ },
320
+ imageTool: {
321
+ margin: "4px 0",
322
+ overflow: "hidden",
323
+ borderRadius: "8px",
324
+ border: "1px solid rgba(120,120,130,0.22)",
325
+ background: "var(--dsh-color-bg-subtle, rgba(120,120,130,0.06))",
326
+ color: "var(--dsh-color-text, inherit)",
327
+ font: "400 12px/1.5 system-ui, sans-serif"
328
+ },
329
+ imageToolToggle: {
330
+ width: "100%",
331
+ display: "flex",
332
+ alignItems: "center",
333
+ gap: "8px",
334
+ padding: "8px 10px",
335
+ cursor: "pointer",
336
+ border: "none",
337
+ background: "transparent",
338
+ color: "inherit",
339
+ textAlign: "left",
340
+ font: "500 12px/1.5 system-ui, sans-serif"
341
+ },
342
+ imageToolStatus: {
343
+ color: "#2FBC44",
344
+ fontSize: "10px"
345
+ },
346
+ imageToolSummary: {
347
+ opacity: .62,
348
+ overflow: "hidden",
349
+ textOverflow: "ellipsis",
350
+ whiteSpace: "nowrap"
351
+ },
352
+ imageToolBody: {
353
+ display: "flex",
354
+ flexDirection: "column",
355
+ gap: "8px",
356
+ padding: "0 10px 10px",
357
+ borderTop: "1px solid rgba(120,120,130,0.14)"
358
+ },
359
+ imageToolText: {
360
+ margin: "8px 0 0",
361
+ whiteSpace: "pre-wrap",
362
+ wordBreak: "break-word",
363
+ opacity: .82
364
+ },
365
+ imageGrid: {
366
+ display: "flex",
367
+ flexWrap: "wrap",
368
+ gap: "8px"
369
+ },
370
+ imageFrame: {
371
+ display: "block",
372
+ maxWidth: "min(320px, 100%)",
373
+ maxHeight: "320px",
374
+ borderRadius: "8px",
375
+ objectFit: "contain",
376
+ background: "rgba(0,0,0,0.08)"
377
+ },
378
+ imageError: {
379
+ padding: "12px",
380
+ color: "#F63218"
319
381
  }
320
382
  };
383
+ /** Pull one image through DSH's own session-authorized attachment RPC. */
384
+ function AuthorizedToolImage({ sessionId, attachment }) {
385
+ const [url, setUrl] = (0, react.useState)(void 0);
386
+ const [failed, setFailed] = (0, react.useState)(false);
387
+ (0, react.useEffect)(() => {
388
+ const controller = new AbortController();
389
+ let objectUrl;
390
+ (async () => {
391
+ try {
392
+ const response = await fetch("/api/session.attachment", {
393
+ method: "POST",
394
+ headers: { "content-type": "application/json" },
395
+ signal: controller.signal,
396
+ body: JSON.stringify({
397
+ type: "client-request",
398
+ rpcId: `pi2dsh-image-${Date.now()}-${Math.random().toString(36).slice(2)}`,
399
+ method: "session.attachment",
400
+ payload: {
401
+ sessionId,
402
+ attachmentId: attachment.attachmentId
403
+ }
404
+ })
405
+ });
406
+ const envelope = await response.json();
407
+ const data = envelope.result?.ok === true ? envelope.result.value?.data : void 0;
408
+ if (!response.ok || typeof data !== "string") throw new Error("attachment unavailable");
409
+ const binary = atob(data);
410
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
411
+ objectUrl = URL.createObjectURL(new Blob([bytes], { type: attachment.mediaType }));
412
+ if (!controller.signal.aborted) setUrl(objectUrl);
413
+ } catch {
414
+ if (!controller.signal.aborted) setFailed(true);
415
+ }
416
+ })();
417
+ return () => {
418
+ controller.abort();
419
+ if (objectUrl !== void 0) URL.revokeObjectURL(objectUrl);
420
+ };
421
+ }, [
422
+ sessionId,
423
+ attachment.attachmentId,
424
+ attachment.mediaType
425
+ ]);
426
+ if (failed) return (0, react.createElement)("div", { style: styles.imageError }, "Image attachment could not be loaded.");
427
+ if (url === void 0) return (0, react.createElement)("div", { style: styles.imageToolText }, "Loading image…");
428
+ return (0, react.createElement)("img", {
429
+ src: url,
430
+ alt: attachment.name ?? "Image returned by Pi tool",
431
+ style: styles.imageFrame,
432
+ "data-pi2dsh": "tool-image"
433
+ });
434
+ }
435
+ function firstArgumentSummary(raw) {
436
+ try {
437
+ const parsed = JSON.parse(raw);
438
+ const preferred = parsed.prompt ?? parsed.description;
439
+ if (typeof preferred === "string") return preferred;
440
+ const first = Object.values(parsed).find((value) => typeof value === "string");
441
+ return typeof first === "string" ? first : "";
442
+ } catch {
443
+ return raw;
444
+ }
445
+ }
446
+ /** Browser row shared by the explicitly supported Pi image tools. */
447
+ function PiImageToolView({ toolName, block, sessionId }) {
448
+ const [expanded, setExpanded] = (0, react.useState)(true);
449
+ const settled = block.kind === "tool-result";
450
+ const argsRaw = settled ? block.call?.argsRaw ?? "" : block.argsRaw ?? "";
451
+ const content = settled && Array.isArray(block.content) ? block.content : [];
452
+ const images = content.flatMap((item) => item.type === "image" && item.attachment !== void 0 ? [item.attachment] : []);
453
+ const text = content.filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text).join("\n");
454
+ const summary = firstArgumentSummary(argsRaw);
455
+ return (0, react.createElement)("div", {
456
+ style: styles.imageTool,
457
+ "data-pi2dsh": "image-tool-result",
458
+ "data-tool": toolName
459
+ }, (0, react.createElement)("button", {
460
+ type: "button",
461
+ style: styles.imageToolToggle,
462
+ "aria-expanded": expanded,
463
+ onClick: () => setExpanded((value) => !value)
464
+ }, (0, react.createElement)("span", { style: styles.imageToolStatus }, block.isError === true ? "●" : settled ? "●" : "◌"), (0, react.createElement)("span", null, toolName), summary === "" ? null : (0, react.createElement)("span", { style: styles.imageToolSummary }, `· ${summary}`)), !expanded ? null : (0, react.createElement)("div", { style: styles.imageToolBody }, text === "" ? null : (0, react.createElement)("div", { style: styles.imageToolText }, text), sessionId === void 0 || images.length === 0 ? null : (0, react.createElement)("div", { style: styles.imageGrid }, ...images.map((attachment) => (0, react.createElement)(AuthorizedToolImage, {
465
+ key: attachment.attachmentId,
466
+ sessionId,
467
+ attachment
468
+ })))));
469
+ }
470
+ /**
471
+ * Register the image row under exact tool names published at package mount.
472
+ * Only known image tools appear in that list, so text-only and native tools
473
+ * retain DSH's existing cards.
474
+ */
475
+ function installImageToolViews(scope) {
476
+ const installed = /* @__PURE__ */ new Set();
477
+ const start = () => {
478
+ let live = true;
479
+ const sync = async () => {
480
+ try {
481
+ const response = await fetch("/pi2dsh/image-tool-names");
482
+ if (!response.ok || !live) return;
483
+ const payload = await response.json();
484
+ if (!Array.isArray(payload.names)) return;
485
+ for (const value of payload.names) {
486
+ if (typeof value !== "string" || value === "" || installed.has(value)) continue;
487
+ installed.add(value);
488
+ scope.slots.inject("tool.call.toolview", () => scope.slots.register({
489
+ name: "tool.call.toolview",
490
+ key: value
491
+ }, PiImageToolView));
492
+ }
493
+ } catch {}
494
+ };
495
+ sync();
496
+ const timer = window.setInterval(() => {
497
+ sync();
498
+ }, POLL_MS);
499
+ return () => {
500
+ live = false;
501
+ window.clearInterval(timer);
502
+ };
503
+ };
504
+ if (scope.effect !== void 0) scope.effect(start, "pi2dsh: image tool views");
505
+ else start();
506
+ }
321
507
  /**
322
508
  * The frame-wide seat: the side-conversation panel, plus whatever packages
323
509
  * pinned frame-wide — transient title and Pi's status entries, as pills.
@@ -479,6 +665,7 @@ window.__ModuleLoader__.load({
479
665
  });
480
666
  });
481
667
  ctx.inject(["slots"], (scope) => {
668
+ installImageToolViews(scope);
482
669
  scope.slots.inject("shell.overlay", () => scope.slots.register({
483
670
  name: "shell.overlay",
484
671
  id: "pi2dsh-overlay",
package/dist/host.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- import { r as registerVisionCompanions, t as applyPiPackage } from "./runtime-DH0zEUpu.mjs";
2
+ import { r as registerVisionCompanions, t as applyPiPackage } from "./runtime-B-SlR8mJ.mjs";
3
3
  import { t as resolvePiPackage } from "./source-0sA5z08z.mjs";
4
4
  import { createRequire } from "node:module";
5
5
  import { readFile } from "node:fs/promises";
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- import { r as registerVisionCompanions } from "./runtime-DH0zEUpu.mjs";
2
+ import { r as registerVisionCompanions } from "./runtime-B-SlR8mJ.mjs";
3
3
  import { t as resolvePiPackage } from "./source-0sA5z08z.mjs";
4
4
  import { applyPiHost, manifestForInstalled } from "./host.mjs";
5
5
  import { a as CONTEXT_RULES, c as PI_AI_PACKAGES, d as UI_CONTEXT_RULES, f as ruleForApi, g as ruleForUiContextProperty, h as ruleForHostImport, i as API_RULES, l as PI_CODING_AGENT_PACKAGES, m as ruleForEvent, n as convertPiMcpConfig, o as EVENT_RULES, p as ruleForContextProperty, r as renderMcpPatch, s as HOST_IMPORT_RULES, t as collectPiMcpServers, u as PI_TUI_PACKAGES } from "./mcp-config-CZepacin.mjs";
@@ -786,6 +786,15 @@ var BrowserSurfaces = class {
786
786
  #draftRequests = /* @__PURE__ */ new Map();
787
787
  #liveDrafts = /* @__PURE__ */ new Map();
788
788
  #completionSources = /* @__PURE__ */ new Map();
789
+ #imageToolNames = /* @__PURE__ */ new Set();
790
+ /** Register one known image tool's exact DSH wire name. */
791
+ registerImageTool(name) {
792
+ if (name.length > 0) this.#imageToolNames.add(name);
793
+ }
794
+ /** Exact wire names needing the generic Pi image-result browser view. */
795
+ imageToolNames() {
796
+ return [...this.#imageToolNames];
797
+ }
789
798
  /**
790
799
  * Track one child session under its parent.
791
800
  * @param parentSessionId - the session the panel floats over.
@@ -1158,6 +1167,14 @@ function registerBrowserSurfaceRoute(ctx, registry) {
1158
1167
  response.end(JSON.stringify({ items }));
1159
1168
  return;
1160
1169
  }
1170
+ if (url.pathname === "/pi2dsh/image-tool-names") {
1171
+ response.writeHead(200, {
1172
+ "content-type": "application/json; charset=utf-8",
1173
+ "cache-control": "no-store"
1174
+ });
1175
+ response.end(method === "HEAD" ? void 0 : JSON.stringify({ names: registry.imageToolNames() }));
1176
+ return;
1177
+ }
1161
1178
  if (url.pathname !== "/pi2dsh/browser-state") {
1162
1179
  response.writeHead(404);
1163
1180
  response.end();
@@ -2549,6 +2566,10 @@ function registerPiProviderRoute(options) {
2549
2566
  //#region src/runtime.ts
2550
2567
  /** Fallback thread ids when a child session reports none. */
2551
2568
  let sidePanelSerial = 0;
2569
+ const KNOWN_IMAGE_TOOLS_BY_PACKAGE = /* @__PURE__ */ new Map([["@crazygit/pi-codex-image-gen", /* @__PURE__ */ new Set(["codex_generate_image"])]]);
2570
+ function isKnownImageTool(packageName, toolName) {
2571
+ return KNOWN_IMAGE_TOOLS_BY_PACKAGE.get(packageName)?.has(toolName) === true;
2572
+ }
2552
2573
  function logger(ctx) {
2553
2574
  const candidate = ctx.logger;
2554
2575
  return {
@@ -4204,16 +4225,17 @@ async function supersedeActiveLogin(state) {
4204
4225
  async function ensureLoggedInProviderRoute(ctx, state, name, config) {
4205
4226
  if (!providerSupportsOAuth(config)) return false;
4206
4227
  if (providerCarriesTransport(config) || state.providerRouteDisposers.has(name)) return false;
4207
- if (llmOf(ctx)?.listProviders().some((provider) => provider.id === name) === true) return false;
4228
+ const alreadyRouted = llmOf(ctx)?.listProviders().some((provider) => provider.id === name) === true;
4208
4229
  if (await storedOAuthCredential(oauthStoreOf(state), name).catch(() => void 0) === void 0) return false;
4230
+ const published = await publishOAuthCredential(ctx, state, name, config);
4231
+ if (!published.ok) return false;
4232
+ keepOAuthCredentialFresh(ctx, state);
4233
+ if (alreadyRouted) return false;
4209
4234
  const settings = optionalService(ctx, "settings");
4210
4235
  if (settings === void 0) {
4211
4236
  logger(ctx).warn(`[pi2dsh] logged in to ${JSON.stringify(name)}, but this composition has no settings service to declare its model route in`);
4212
4237
  return false;
4213
4238
  }
4214
- const published = await publishOAuthCredential(ctx, state, name, config);
4215
- if (!published.ok) return false;
4216
- keepOAuthCredentialFresh(ctx, state);
4217
4239
  await settings.update("llm-pi-ai", { providers: { [name]: {
4218
4240
  ...typeof config.name === "string" ? { displayName: config.name } : {},
4219
4241
  ...published.baseUrl === void 0 ? {} : { baseURL: published.baseUrl },
@@ -4460,6 +4482,7 @@ function registerTool(ctx, state, tool) {
4460
4482
  };
4461
4483
  const dispose = ctx.tools.register(definition);
4462
4484
  state.toolDisposers.set(tool.name, dispose);
4485
+ if (isKnownImageTool(state.packageName, tool.name)) state.shared.browserSurfaces?.registerImageTool(tool.name);
4463
4486
  }
4464
4487
  function unregisterTool(state, name) {
4465
4488
  const dispose = state.toolDisposers.get(name);
@@ -5176,7 +5199,11 @@ async function applyPiPackage(ctx, options) {
5176
5199
  oauth: provider.auth.oauth
5177
5200
  });
5178
5201
  ensureLoginCommand(ctx, state);
5179
- for (const [name, config] of [...state.providers]) ensureLoggedInProviderRoute(ctx, state, name, config).catch((error) => logger(ctx).warn(`[pi2dsh] could not restore the route for logged-in provider ${JSON.stringify(name)}: ${error instanceof Error ? error.message : String(error)}`));
5202
+ for (const [name, config] of [...state.providers]) try {
5203
+ await ensureLoggedInProviderRoute(ctx, state, name, config);
5204
+ } catch (error) {
5205
+ logger(ctx).warn(`[pi2dsh] could not restore the route for logged-in provider ${JSON.stringify(name)}: ${error instanceof Error ? error.message : String(error)}`);
5206
+ }
5180
5207
  if (options.manifest.skillDirs.length > 0) {
5181
5208
  if (ctx.get("skills") === void 0) logger(ctx).warn("[pi2dsh] migrated skills were not mounted because this DSH composition has no ctx.skills");
5182
5209
  else {
@@ -5335,6 +5362,7 @@ const runtimeInternals = {
5335
5362
  resolveOfferedChoice,
5336
5363
  dshToPiContent,
5337
5364
  expandPrompt,
5365
+ isKnownImageTool,
5338
5366
  isSubagentOrigin,
5339
5367
  normalizeToolResult,
5340
5368
  splitArguments,
@@ -5344,4 +5372,4 @@ const runtimeInternals = {
5344
5372
  //#endregion
5345
5373
  export { runtimeInternals as i, normalizeToolSchema as n, registerVisionCompanions as r, applyPiPackage as t };
5346
5374
 
5347
- //# sourceMappingURL=runtime-DH0zEUpu.mjs.map
5375
+ //# sourceMappingURL=runtime-B-SlR8mJ.mjs.map