@buzzni/saycode-cli 0.9.7 → 0.9.9

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
@@ -136,8 +136,9 @@ Happy CLI 설치본은 동일한 agent 명령을 `happy agent` facade로 제공
136
136
  먼저 `happy agent whoami`로 capability와 spawn 한도를 확인해야 합니다.
137
137
 
138
138
  ```bash
139
- happy agent ls --status # 스코프 안 세션의 상태·대기 사유·마지막 응답·seq 커서
139
+ happy agent ls --status # 스코프 안 세션의 상태·작업 맥락·연락 방식(statusVersion:2)
140
140
  happy agent spawn --prompt "변경사항을 읽기 전용으로 리뷰해줘"
141
+ happy agent spawn --prompt "위험도를 판정해줘" --output-schema '{"type":"object","properties":{"risk":{"enum":["low","high"]}},"required":["risk"]}'
141
142
  happy agent prompt <id> "리뷰 근거를 더 설명해줘"
142
143
  happy agent wait --any <id1> <id2> --until turn-end # 여럿 중 하나라도 끝나면 반환
143
144
  happy agent read <id> --since <seq> # 마지막으로 읽은 뒤의 변경분만
@@ -147,6 +148,40 @@ happy agent steer <id> <text> # 지금 생성 중인 응답의 방향만 수
147
148
  happy agent stop <id> # 사용자가 명시한 경우에만 현재 응답 중단
148
149
  ```
149
150
 
151
+ `ls --status` v2는 전체 `id`, 제목(80자)·요약(240자), 사람 표시 정보(`verified:false`), 머신,
152
+ 저장소·브랜치, `runtime`, 최근 작업 `activity`, `observedAt`·`activityObservedAt`·`stale`,
153
+ `ownership`, `contact.prompt/steer/stop`의 상태·사유·실행 영향을 보여 줍니다. 사람 정보는
154
+ `createdBy`의 주장일 뿐 소유 증명이 아닙니다. 저장소 정본 정보는 없어 `repository:null`, 브랜치는
155
+ `metadata.worktree.branch`만 쓰며 빈 값과 ID 제목은 null입니다. 동명 대화도 전체 ID로 선택하세요.
156
+ 최근 50개 원본 메시지의 Claude TodoWrite·Codex plan에서 진행 중 작업 3개만 읽습니다. 빈/완료 목록은
157
+ none, 창 밖·ACP plan 미저장은 사유가 있는 unknown입니다. 재개 뒤 이전 조회의 작업을 되살리지 않습니다.
158
+ Claude의 `activity.source:TodoWrite`와 sourceMessageId/sourceSeq는 호출 시작 시 입력 목록의 출처이며
159
+ 쓰기 성공을 증명하지 않습니다. 현재 happy-cli mapper가 개별 tool_result의 `is_error`를 보내지 않아,
160
+ TodoWrite가 실패했어도 오류 정보 없는 tool-call-end 뒤 턴이 completed이면 관측한 목록을 유지합니다.
161
+ 개별 실패 탐지는 알려진 검증 한계입니다. 향후 같은 call ID의 `is_error:true`가 실제로 오면 unknown으로
162
+ 내립니다. 실패/취소된 턴은 별도로 unknown 처리하며, 무관한 call이나 subagent 오류를 결속하지 않습니다.
163
+ 조회 실패는 상태도 unknown이며 오류 원문을 노출하지 않습니다. 최신순 최대 100행·UTF-8 768KiB,
164
+ 동시 조회 4개이며 초과하면 `truncated:true`, `candidateCount`는 읽기 허용 후보 수만 셉니다.
165
+
166
+ `contact`는 T1b 실제 verb 게이트의 거부(`reason`·`authorizationReason`)를 그대로 반영하고 그 뒤에
167
+ 대상 runtime 능력과 실행 상태를 표시합니다. 조회 가능과 변경 가능은 다릅니다. `available`도 실행 보장이
168
+ 아니며 실제 명령에서 재검사합니다. 120초보다 오래된 `stale:true` 또는 시간 미확인의 `stale:unknown`은
169
+ 연락 전에 다시 확인하세요. no-wake는 미지원, Claude local은 switch-required(자동 전환 없음),
170
+ 능력 미광고인 기존 pin은 unknown입니다. P1 실앱 수락은 Claude 비 mandatory·Codex account broker 허브이며
171
+ Gemini·opencode·grok 직접 허브는 P1 실앱 수락 범위 밖입니다(실제 권한 표시는 유지, steer 미지원).
172
+ mandatory Claude는 `not_agent_env`라 행을 만들지 않습니다. 기본 `ls` 출력 계약은 그대로입니다.
173
+
174
+ `spawn --output-schema`는 inline JSON 또는 절대 경로의 JSON Schema(draft-07, 모르는 키워드·`$async` 불가 — 번들 Claude CLI와 같은 규칙)를 받아, 그 자식(Claude만)의 **세션 전체**
175
+ — 이후 `agent prompt`로 보낸 턴까지 — 를 그 schema로 답하게 합니다. 한 세션에 query 하나를 유지하는 런타임 구조라
176
+ 턴마다 schema를 바꿀 수 없기 때문입니다. `wait`/`prompt --wait`는 그 턴의 검증된 값을 `structuredOutput`으로 돌려주고,
177
+ 턴이 끝났는데 값이 없으면(`subtype: success`여도) `structured_output_missing`으로 실패합니다. `read`는 실패 없이
178
+ `structuredOutput.status`를 보여 주고, `wait --any`는 만족된 세션마다 판정을 `structuredOutput[<id>]`에 담을 뿐
179
+ 대기 전체를 실패시키지 않습니다. 취소된 턴도 `missing`(`turn_cancelled`)으로 끝나며, `/compact` 턴과 일시적 API
180
+ 오류로 자동 재시도되기 전의 실패 턴도 각각 `missing`으로 보입니다. 머신 런타임이 `structuredOutput` capability를
181
+ 광고하지 않으면 `structured_output_unsupported`로 거절하고 세션을 만들지 않습니다(구버전 daemon은 필드를 조용히
182
+ 버리기 때문). 구조화 세션의 마지막 답은 schema JSON이라 `<saycode-complete>` 완료 신호가 나오지 않으므로
183
+ `--until completed` 대신 `--until turn-end`로 기다리세요. 일반 세션의 완료 신호 계약은 바뀌지 않습니다.
184
+
150
185
  `read --around`는 표시 창만 옮깁니다 — `completion`·`lastSeq`는 여전히 최신 창 기준이라 `--since` 커서와 함께 쓸 수
151
186
  있고, 둘을 한 호출에 같이 주지는 못합니다. 없는 seq는 오류가 아니라 `around.found:false`와 가장 가까운 메시지로 답합니다.
152
187
  `read`는 계정 최신 150개 목록에서 밀려난 세션도 **정확한 id**로 엽니다(같은 스코프 판정, 이름·접두 매칭 없음).
@@ -169,10 +204,18 @@ saycode를 따로 설치하지 않은 작업PC는 `npx @buzzni/saycode-cli searc
169
204
  `--worktree`로 만든 자식만 자기 worktree를 `root`로 갖고, `scope`는 그대로 부모의 것을 물려받습니다
170
205
  (ADR-065) — 그래야 부모가 계속 그 자식을 보고 제어할 수 있습니다.
171
206
 
207
+ 부모 세션의 런타임이 지금 턴의 분류(`SAYCODE_DELEGATED_ATTENDANCE`: `unattended`·`attended`)를 넘겨 주면
208
+ `spawn`(대화를 넘기는 `spawn --handoff` 포함)은 그 값을 자식 env에, `prompt`는 자기가 spawn한 자손에게 보내는 메시지 meta(`delegatedAttendance`)에만
209
+ 싣습니다(사람의 대화 등 그 밖의 세션에는 싣지 않아 그 대화는 계속 사람의 답을 기다립니다). 예약 작업처럼
210
+ 지켜보는 사람이 없는 턴이 띄운 자식이나 넘겨받은 새 대화는 승인을 영원히 기다리지 않고 런타임의 무인 대기 시간 뒤 거절로
211
+ 마무리합니다(specs/codexcli-approval-session-binding R12). 값이 없거나 모르는 값이면 아무것도 싣지 않고,
212
+ 자식은 지금처럼 사람의 답을 기다립니다.
213
+
172
214
  `steer` 성공의 `accepted:true`는 세션 RPC가 요청을 받았다는 뜻이며 실제 transcript 전달 완료를
173
- 뜻하지 않습니다(`delivered:false`). 끝난 턴의 후속 작업에는 `prompt`를 사용합니다. 사람이 시작한 세션
174
- (`depth` 0)은 스코프 안 모든 세션에 `steer`와 `stop`을 보낼 수 있고, spawn된 세션은 원장에 기록된 최대
175
- 2단계 자식만 허용하며, `stop`은 보관·삭제를 하지 않습니다. spawn 한도(`whoami`의 `maxSpawn`)는 세션이
215
+ 뜻하지 않습니다(`delivered:false`). 끝난 턴의 후속 작업에는 `prompt`를 사용합니다. 에이전트 세션은
216
+ 자기 자신에게 입력할 수 없고, `steer`와 `stop`은 depth와 관계없이 원장에 기록된 자기 후손에게만 보낼 수
217
+ 있습니다. 후손이 아닌 세션에 `prompt`하려면 소유가 검증돼야 하며, `not_owned`를 받으면 `read`로만 보고
218
+ 다시 시도하지 않습니다(ADR-078). `stop`은 보관·삭제를 하지 않습니다. spawn 한도(`whoami`의 `maxSpawn`)는 세션이
176
219
  도는 머신의 코어·메모리에서 산정하므로 머신마다 다르고, 최근 6시간 안의 spawn만 세는 롤링 윈도우라
177
220
  소진돼도 시간이 지나면 회복됩니다(`budgetResetsAt`, ADR-063). 데몬이 확인하지 못한 시도도 같은
178
221
  6시간을 차감합니다 — 응답만 유실됐을 뿐 세션은 만들어졌을 수 있기 때문입니다. 깊이 상한에는 이런
@@ -188,6 +231,12 @@ worktree는 계정 로그인이 있어야 만들 수 있고, 부모에 커밋되
188
231
  ref는 부모 HEAD로 폴백하지 않고 거절합니다. 결과 JSON의 `worktreeId`·`branch`·
189
232
  `baseSha`로 나중에 결과를 회수하며, 합류(merge·PR)는 자동으로 하지 않습니다.
190
233
 
234
+ 격리된 세션의 broker 경로도 관리형 worktree 생성을 지원합니다. Git 상태는 격리된 CLI에서 읽고,
235
+ worktree 생성도 같은 격리 client가 수행합니다. 인증을 가진 host는 현재 프로젝트·머신을 확인하고
236
+ 기존 관리형 등록 API로 결과를 검증한 뒤 자식을 시작합니다. CLI client와 내장
237
+ host에 모두 이 지원이 필요합니다. 오래된 조합에서 Git snapshot이 없으면 업데이트 안내와 함께
238
+ 거절하며, 같은 폴더 생성으로 자동 전환하지 않습니다.
239
+
191
240
  ## 긴 prompt 작성과 재사용
192
241
 
193
242
  TTY의 유휴 chat prompt에서 `/edit-prompt`를 입력하면 외부 편집기가 열립니다. 편집기는 `$VISUAL`,
@@ -309,7 +358,8 @@ emoji의 표시 폭도 반영합니다. 링크는 파이프 출력에서도 목
309
358
  GUI 없는 머신(Debian/Ubuntu, macOS — Windows는 `run`만)에서 Saycode 서버와 중계를 직접 운영합니다. 소유자 1명용이며,
310
359
  서버 머신 자신도 작업 머신이 됩니다. 휴대폰·Desktop·이 CLI와 다른 작업 머신이 이 서버에 붙습니다.
311
360
 
312
- **준비물**: Node 20 이상. Linux는 `sudo apt install openssl python3 make g++`가 필요합니다. openssl이 없으면 서버가 시작
361
+ **준비물**: Node 20 이상. Node 20이면 npm 11 이상도 필요합니다(`npm install -g npm@11`) — Node 20에서는 npm 10(함께 설치되는 10.8.2와
362
+ 10.9.9 확인)이 `init`의 서버 패키지 설치 중 `Cannot read properties of null (reading 'edgesOut')`로 실패합니다. Node 22.22 이상은 기본 npm(10.9)으로 됩니다. Linux는 `sudo apt install openssl python3 make g++`가 필요합니다. openssl이 없으면 서버가 시작
313
363
  직후 멈춥니다. 빌드 도구는 아키텍처와 관계없이(x64·arm64) `happy-cli`의 `node-pty`를 소스에서 빌드하는 데 씁니다. 서버 머신도
314
364
  자기 daemon을 위해 `happy-cli`를 설치하므로 함께 필요합니다. 에이전트를 쓰려면 각 머신에서 Claude Code·Codex 로그인이 필요합니다.
315
365
 
@@ -48,6 +48,93 @@ function contributionList(value, path2, allowedKeys, parse) {
48
48
  return parse(entry, entryPath);
49
49
  });
50
50
  }
51
+ const ENTRYPOINT_PATTERN = /\.(?:cjs|mjs|js)$/;
52
+ function relativePath(value, path2, requireScript = false) {
53
+ const parsed = string(value, path2);
54
+ const segments = parsed.split("/");
55
+ if (parsed.startsWith("/") || parsed.includes("\\") || segments.some((segment) => segment === "" || segment === "." || segment === "..") || requireScript && !ENTRYPOINT_PATTERN.test(parsed)) {
56
+ fail$5(path2, "expected safe relative path");
57
+ }
58
+ return parsed;
59
+ }
60
+ function keys$1(value, allowed) {
61
+ const result = record$1(value, "widget");
62
+ assertOnlyKeys(result, new Set(allowed), "widget");
63
+ if (allowed.some((key) => !Object.hasOwn(result, key))) fail$5("widget", "missing field");
64
+ return result;
65
+ }
66
+ function text$3(value, limit = 4096) {
67
+ if (typeof value !== "string" || new TextEncoder().encode(value).length > limit) fail$5("widget", "invalid text");
68
+ return value;
69
+ }
70
+ function number(value, max = Number.MAX_SAFE_INTEGER) {
71
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > max) fail$5("widget", "invalid number");
72
+ return value;
73
+ }
74
+ function bounded(value) {
75
+ const encoded = JSON.stringify(value);
76
+ if (!encoded || new TextEncoder().encode(encoded).length > 32768) fail$5("widget", "payload exceeds 32 KiB");
77
+ }
78
+ function parseChatWidgets(value, apiVersion) {
79
+ if (apiVersion !== 5) fail$5("manifest.contributes.chatWidgets", "requires Extension API version 5");
80
+ const result = contributionList(value, "manifest.contributes.chatWidgets", ["id", "title", "order"], (entry) => ({
81
+ id: id(entry.id, "widget.id"),
82
+ title: text$3(entry.title, 256),
83
+ order: number(entry.order ?? 0, 1e3)
84
+ }));
85
+ if (result.length > 2) fail$5("manifest.contributes.chatWidgets", "at most two widgets");
86
+ return result;
87
+ }
88
+ function parseWidgetTree(value) {
89
+ let count = 0;
90
+ const walk = (input, depth) => {
91
+ if (++count > 100 || depth > 8) fail$5("widget", "tree limit exceeded");
92
+ const node = record$1(input, "widget");
93
+ if (node.type === "text") {
94
+ keys$1(node, ["type", "text"]);
95
+ return { type: "text", text: text$3(node.text) };
96
+ }
97
+ if (node.type === "metric") {
98
+ keys$1(node, ["type", "label", "value", "tone"]);
99
+ if (node.tone !== "neutral" && node.tone !== "warning") fail$5("widget", "invalid tone");
100
+ return { type: "metric", label: text$3(node.label), value: text$3(node.value), tone: node.tone };
101
+ }
102
+ keys$1(node, ["type", "children"]);
103
+ if (node.type !== "list" || !Array.isArray(node.children)) fail$5("widget", "invalid node");
104
+ return { type: "list", children: node.children.map((child) => walk(child, depth + 1)) };
105
+ };
106
+ const result = walk(value, 1);
107
+ bounded(result);
108
+ return result;
109
+ }
110
+ function parseWidgetSnapshot(value) {
111
+ const row2 = keys$1(value, ["version", "language", "provider", "model", "running", "context", "quota", "goal"]);
112
+ if (row2.version !== 1 || typeof row2.running !== "boolean") fail$5("widget", "invalid snapshot");
113
+ const context = row2.context === null ? null : keys$1(row2.context, ["usedTokens", "limitTokens", "observedAt"]);
114
+ const quota = keys$1(row2.quota, ["state", "reason", "observedAt", "windows"]);
115
+ const states = ["fresh", "partial", "stale", "unavailable", "unsupported", "scope-missing", "probe-disabled", "backing-off"];
116
+ if (!states.includes(String(quota.state)) || !Array.isArray(quota.windows) || quota.windows.length > 2) fail$5("widget", "invalid quota");
117
+ if (!["fresh", "partial"].includes(String(quota.state)) && quota.windows.length) fail$5("widget", "unavailable quota has numbers");
118
+ const windows = quota.windows.map((value2) => {
119
+ const window = keys$1(value2, ["kind", "usedPercent", "resetsAt"]);
120
+ if (window.kind !== "fiveHour" && window.kind !== "sevenDay") fail$5("widget", "invalid window");
121
+ return { kind: window.kind, usedPercent: number(window.usedPercent, 100), resetsAt: window.resetsAt === null ? null : number(window.resetsAt) };
122
+ });
123
+ if (new Set(windows.map((window) => window.kind)).size !== windows.length) fail$5("widget", "duplicate window");
124
+ const goal = row2.goal === null ? null : keys$1(row2.goal, ["status", "text"]);
125
+ const result = {
126
+ version: 1,
127
+ language: text$3(row2.language, 8),
128
+ provider: text$3(row2.provider, 64),
129
+ model: row2.model === null ? null : text$3(row2.model, 128),
130
+ running: row2.running,
131
+ context: context ? { usedTokens: number(context.usedTokens), limitTokens: number(context.limitTokens), observedAt: context.observedAt === null ? null : number(context.observedAt) } : null,
132
+ quota: { state: String(quota.state), reason: quota.reason === null ? null : text$3(quota.reason, 256), observedAt: quota.observedAt === null ? null : number(quota.observedAt), windows },
133
+ goal: goal ? { status: text$3(goal.status, 64), text: text$3(goal.text) } : null
134
+ };
135
+ bounded(result);
136
+ return result;
137
+ }
51
138
  function parseExtensionFileViewers(value, apiVersion) {
52
139
  if (apiVersion < 4) fail$5("manifest.contributes.fileViewers", "requires Extension API version 4");
53
140
  return contributionList(value, "manifest.contributes.fileViewers", ["id", "title", "extensions", "panelId"], (entry, path2) => {
@@ -60,6 +147,23 @@ function assertExtensionFileViewers(extensionId2, viewers, panels, permissions)
60
147
  if (!permissions.includes("filePreviews.read") || !viewer.id.startsWith(`${extensionId2}.`) || !viewer.panelId.startsWith(`${extensionId2}.`) || !panels.some((panel) => panel.id === viewer.panelId)) fail$5("manifest.contributes.fileViewers", "requires filePreviews.read and an owned panel");
61
148
  }
62
149
  }
150
+ const ELEMENT_PICK_INSPECT_PERMISSION = "browserPicks.inspect";
151
+ function parseExtensionElementPickInspectors(value, apiVersion) {
152
+ if (apiVersion < 4) fail$5("manifest.contributes.elementPickInspectors", "requires Extension API version 4");
153
+ return contributionList(value, "manifest.contributes.elementPickInspectors", ["id", "title", "script"], (entry, path2) => ({
154
+ id: id(entry.id, `${path2}.id`),
155
+ title: string(entry.title, `${path2}.title`),
156
+ script: relativePath(entry.script, `${path2}.script`, true)
157
+ }));
158
+ }
159
+ function assertExtensionElementPickInspectors(extensionId2, inspectors, permissions) {
160
+ if (inspectors.length > 0 !== permissions.includes(ELEMENT_PICK_INSPECT_PERMISSION)) {
161
+ fail$5("manifest.contributes.elementPickInspectors", `requires ${ELEMENT_PICK_INSPECT_PERMISSION} permission and an inspector that uses it`);
162
+ }
163
+ for (const inspector of inspectors) {
164
+ if (!inspector.id.startsWith(`${extensionId2}.`)) fail$5("manifest.contributes.elementPickInspectors", "expected an id declared by the same extension");
165
+ }
166
+ }
63
167
  function parseMachineCommands(value, extensionId2, apiVersion) {
64
168
  if (apiVersion < 3) fail$5("manifest.machineCommands", "requires Extension API version 3");
65
169
  const root = record$1(value, "manifest.machineCommands");
@@ -492,6 +596,8 @@ function parseExtensionChannelContributions(value) {
492
596
  });
493
597
  }
494
598
  const EXTENSION_PERMISSIONS = [
599
+ "sessionContext.read",
600
+ "sessionContext.subscribe",
495
601
  "assets.read",
496
602
  "assets.write",
497
603
  "files.select",
@@ -514,6 +620,7 @@ const EXTENSION_PERMISSIONS = [
514
620
  "browserViewer.installChrome",
515
621
  "artifacts.publishPublic",
516
622
  "filePreviews.read",
623
+ "browserPicks.inspect",
517
624
  // One permission per channel wire method (`src/domain/channelProtocol.ts`), deliberately not a
518
625
  // single adapter grant. Receiving and acking messages is transport work; reading a session,
519
626
  // driving it, or approving a tool call once are three larger and separately-refusable
@@ -528,8 +635,9 @@ const EXTENSION_PERMISSIONS = [
528
635
  "sessions.control",
529
636
  "sessions.approveOnce"
530
637
  ];
531
- const SAYCODE_EXTENSION_API_VERSION = 4;
532
- const SAYCODE_EXTENSION_MINIMUM_API_VERSION = 2;
638
+ const CONVERSATION_MODS_INTERNAL = typeof __SAYCODE_MODS_INTERNAL__ !== "undefined" && __SAYCODE_MODS_INTERNAL__ === true;
639
+ const SAYCODE_EXTENSION_API_VERSION = CONVERSATION_MODS_INTERNAL ? 5 : 4;
640
+ const SAYCODE_EXTENSION_MINIMUM_API_VERSION = CONVERSATION_MODS_INTERNAL ? 4 : 2;
533
641
  const MAX_EXTENSION_API_WINDOW_WIDTH = 2;
534
642
  const SAYCODE_EXTENSION_ENGINE_VERSION = "1.2.0";
535
643
  const SAYCODE_CHANNEL_API_VERSION = CHANNEL_API_VERSION;
@@ -552,9 +660,8 @@ class UnsupportedExtensionApiVersionError extends Error {
552
660
  }
553
661
  const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
554
662
  const ENGINE_RANGE_PATTERN = /^(?:\^|~|>=|<=|>|<)?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
555
- const ENTRYPOINT_PATTERN = /\.(?:cjs|mjs|js)$/;
556
663
  const LOCALE_PATTERN = /^[a-z]{2,3}(?:-[A-Z]{2})?$/;
557
- const CONTRIBUTION_KEYS = /* @__PURE__ */ new Set(["commands", "settings", "panels", "projectTemplates", "machineActions", "projectTools", "artifactActions", "channels", "fileViewers"]);
664
+ const CONTRIBUTION_KEYS = /* @__PURE__ */ new Set(["chatWidgets", "commands", "settings", "panels", "projectTemplates", "machineActions", "projectTools", "artifactActions", "channels", "fileViewers", "elementPickInspectors"]);
558
665
  const V2_PERMISSIONS = /* @__PURE__ */ new Set([
559
666
  "browserViewer.open",
560
667
  "browserViewer.install",
@@ -626,14 +733,6 @@ function parseApiVersion(value, options) {
626
733
  }
627
734
  return value;
628
735
  }
629
- function relativePath(value, path2, requireScript = false) {
630
- const parsed = string(value, path2);
631
- const segments = parsed.split("/");
632
- if (parsed.startsWith("/") || parsed.includes("\\") || segments.some((segment) => segment === "" || segment === "." || segment === "..") || requireScript && !ENTRYPOINT_PATTERN.test(parsed)) {
633
- fail$5(path2, "expected safe relative path");
634
- }
635
- return parsed;
636
- }
637
736
  function parseChannelApiVersion(value, apiVersion) {
638
737
  if (value === void 0) return void 0;
639
738
  if (apiVersion < 3) fail$5("manifest.channelApiVersion", "requires Extension API version 3");
@@ -657,6 +756,7 @@ function parseContributions(value, apiVersion, channelApiVersion) {
657
756
  }
658
757
  const result = {};
659
758
  if (parsed.fileViewers !== void 0) result.fileViewers = parseExtensionFileViewers(parsed.fileViewers, apiVersion);
759
+ if (parsed.elementPickInspectors !== void 0) result.elementPickInspectors = parseExtensionElementPickInspectors(parsed.elementPickInspectors, apiVersion);
660
760
  if (parsed.commands !== void 0) {
661
761
  result.commands = contributionList(
662
762
  parsed.commands,
@@ -762,6 +862,7 @@ function parseContributions(value, apiVersion, channelApiVersion) {
762
862
  }
763
863
  );
764
864
  }
865
+ if (parsed.chatWidgets !== void 0) result.chatWidgets = parseChatWidgets(parsed.chatWidgets, apiVersion);
765
866
  if (parsed.projectTools !== void 0) result.projectTools = parseExtensionProjectTools(parsed.projectTools, apiVersion);
766
867
  if (parsed.artifactActions !== void 0) {
767
868
  result.artifactActions = parseExtensionArtifactActions(parsed.artifactActions);
@@ -770,6 +871,7 @@ function parseContributions(value, apiVersion, channelApiVersion) {
770
871
  result.channels = parseExtensionChannelContributions(parsed.channels);
771
872
  }
772
873
  const ids = [
874
+ ...result.chatWidgets ?? [],
773
875
  ...result.commands ?? [],
774
876
  ...result.settings ?? [],
775
877
  ...result.panels ?? [],
@@ -777,7 +879,8 @@ function parseContributions(value, apiVersion, channelApiVersion) {
777
879
  ...result.machineActions ?? [],
778
880
  ...result.projectTools ?? [],
779
881
  ...result.artifactActions ?? [],
780
- ...result.channels ?? []
882
+ ...result.channels ?? [],
883
+ ...result.elementPickInspectors ?? []
781
884
  ].map((entry) => entry.id);
782
885
  if (new Set(ids).size !== ids.length) fail$5("manifest.contributes", "duplicate contribution id");
783
886
  return result;
@@ -808,7 +911,9 @@ function parseExtensionManifest(value, options) {
808
911
  fail$5("manifest.permissions", `unknown permission ${parsed}`);
809
912
  }
810
913
  const typed = parsed;
914
+ if (typed.startsWith("sessionContext.") && apiVersion !== 5) fail$5("manifest.permissions", "sessionContext requires API 5");
811
915
  if (typed === "filePreviews.read" && apiVersion < 4) fail$5("manifest.permissions", "filePreviews.read requires Extension API version 4");
916
+ if (typed === "browserPicks.inspect" && apiVersion < 4) fail$5("manifest.permissions", "browserPicks.inspect requires Extension API version 4");
812
917
  if (apiVersion < 2 && V2_PERMISSIONS.has(typed)) {
813
918
  fail$5(`manifest.permissions[${index}]`, "requires Extension API version 2");
814
919
  }
@@ -836,6 +941,12 @@ function parseExtensionManifest(value, options) {
836
941
  (event, index) => string(event, `manifest.activationEvents[${index}]`)
837
942
  );
838
943
  const contributes = parseContributions(manifest.contributes, apiVersion, channelApiVersion);
944
+ const widgets = contributes.chatWidgets ?? [];
945
+ const widgetPermissions = permissions.filter((p) => p.startsWith("sessionContext."));
946
+ if (widgets.length > 0 !== widgetPermissions.length > 0 || widgets.length > 0 && widgetPermissions.length !== 2) fail$5("manifest.contributes.chatWidgets", "requires both sessionContext permissions and widgets");
947
+ for (const widget of widgets) {
948
+ if (!widget.id.startsWith(`${parsedId}.`) || !activationEvents.includes(`onChatWidget:${widget.id}`)) fail$5("manifest.contributes.chatWidgets", "requires same-owner id and activation event");
949
+ }
839
950
  if (contributes.channels?.length && !permissions.includes("channels.receive")) {
840
951
  fail$5("manifest.contributes.channels", "requires channels.receive permission");
841
952
  }
@@ -881,6 +992,7 @@ function parseExtensionManifest(value, options) {
881
992
  }
882
993
  }
883
994
  assertExtensionFileViewers(parsedId, contributes.fileViewers ?? [], contributes.panels ?? [], permissions);
995
+ assertExtensionElementPickInspectors(parsedId, contributes.elementPickInspectors ?? [], permissions);
884
996
  assertExtensionProjectToolTargets(parsedId, contributes.projectTools ?? [], contributes.commands ?? []);
885
997
  return {
886
998
  id: parsedId,
@@ -989,6 +1101,9 @@ function parseRequest(message) {
989
1101
  const method = text(message.method, "request.method");
990
1102
  const params = plainRecord(message.params, "request.params");
991
1103
  switch (method) {
1104
+ case "widget.render":
1105
+ exactKeys(params, ["widgetId", "snapshot"], "request.params");
1106
+ return { kind: "request", id: id2, method, params: { widgetId: text(params.widgetId, "widgetId"), snapshot: parseWidgetSnapshot(params.snapshot) } };
992
1107
  case "handshake":
993
1108
  exactKeys(params, ["apiVersion", "extensionId", "extensionVersion"], "request.params");
994
1109
  return {
@@ -1027,6 +1142,13 @@ function parseRequest(message) {
1027
1142
  connectionRevision: Number(params.connectionRevision)
1028
1143
  } };
1029
1144
  }
1145
+ case "channel.intake": {
1146
+ exactKeys(params, ["apiVersion", "channelId", "connectionId", "provider", "connectionRevision"], "request.params");
1147
+ const lifecycle = parseRequest({ kind: "request", id: id2, method: "channel.lifecycle", params: { ...params, state: "start" } });
1148
+ if (lifecycle.method !== "channel.lifecycle") fail("invalid channel intake");
1149
+ const { state: _state, ...identity } = lifecycle.params;
1150
+ return { kind: "request", id: id2, method, params: identity };
1151
+ }
1030
1152
  case "channel.formatReply": {
1031
1153
  const formatKeys = [
1032
1154
  "apiVersion",
@@ -1172,8 +1294,10 @@ class ExtensionHostRuntime {
1172
1294
  constructor(options) {
1173
1295
  this.options = options;
1174
1296
  }
1297
+ widgets = /* @__PURE__ */ new Map();
1175
1298
  channelHandlers = /* @__PURE__ */ new Map();
1176
1299
  channelFormatters = /* @__PURE__ */ new Map();
1300
+ channelIntakeHandlers = /* @__PURE__ */ new Map();
1177
1301
  channelStates = /* @__PURE__ */ new Map();
1178
1302
  channelCalls = /* @__PURE__ */ new Map();
1179
1303
  channelEpoch = 0;
@@ -1304,9 +1428,11 @@ class ExtensionHostRuntime {
1304
1428
  } finally {
1305
1429
  this.active = false;
1306
1430
  this.module = null;
1431
+ this.widgets.clear();
1307
1432
  this.commandHandlers.clear();
1308
1433
  this.channelHandlers.clear();
1309
1434
  this.channelFormatters.clear();
1435
+ this.channelIntakeHandlers.clear();
1310
1436
  this.channelStates.clear();
1311
1437
  this.channelCalls.clear();
1312
1438
  }
@@ -1345,9 +1471,11 @@ class ExtensionHostRuntime {
1345
1471
  if (this.active) await this.module?.deactivate?.(request.params.reason);
1346
1472
  } finally {
1347
1473
  this.active = false;
1474
+ this.widgets.clear();
1348
1475
  this.commandHandlers.clear();
1349
1476
  this.channelHandlers.clear();
1350
1477
  this.channelFormatters.clear();
1478
+ this.channelIntakeHandlers.clear();
1351
1479
  this.channelStates.clear();
1352
1480
  this.channelCalls.clear();
1353
1481
  this.deactivating = false;
@@ -1360,8 +1488,14 @@ class ExtensionHostRuntime {
1360
1488
  if (!this.active) {
1361
1489
  throw { code: "NOT_ACTIVE", message: "extension command handler is not active" };
1362
1490
  }
1491
+ if (request.method === "widget.render") {
1492
+ const render = this.widgets.get(request.params.widgetId);
1493
+ if (!render) throw new Error("widget is not registered");
1494
+ return parseWidgetTree(await render(request.params.snapshot));
1495
+ }
1363
1496
  if (request.method === "channel.lifecycle") return this.deliverChannel(request.params);
1364
1497
  if (request.method === "channel.formatReply") return this.formatChannelReply(request.params);
1498
+ if (request.method === "channel.intake") return this.deliverChannelIntake(request.params);
1365
1499
  const handler = this.commandHandlers.get(request.params.commandId);
1366
1500
  if (!handler) {
1367
1501
  throw { code: "COMMAND_NOT_REGISTERED", message: `extension command is not registered: ${request.params.commandId}` };
@@ -1410,6 +1544,14 @@ class ExtensionHostRuntime {
1410
1544
  if (this.channelCalls.get(key) === next) this.channelCalls.delete(key);
1411
1545
  }
1412
1546
  }
1547
+ /** A hint for a live connection only; anything else is answered and dropped, never an error. */
1548
+ async deliverChannelIntake(input) {
1549
+ const current = this.channelStates.get(JSON.stringify([input.channelId, input.connectionId]));
1550
+ const handler = this.channelIntakeHandlers.get(input.channelId);
1551
+ if (!handler || this.deactivating || current?.state !== "start" || current.provider !== input.provider || current.connectionRevision !== input.connectionRevision) return null;
1552
+ await handler(Object.freeze({ ...input }));
1553
+ return null;
1554
+ }
1413
1555
  async formatChannelReply(input) {
1414
1556
  const key = JSON.stringify([input.channelId, input.connectionId]);
1415
1557
  const current = this.channelStates.get(key);
@@ -1450,11 +1592,17 @@ class ExtensionHostRuntime {
1450
1592
  await this.options.loadModule(this.options.manifest.entrypoint)
1451
1593
  );
1452
1594
  if (this.disposed) throw { code: "HOST_DISPOSED", message: "extension host is disposed" };
1595
+ const registeredWidgets = /* @__PURE__ */ new Map();
1453
1596
  const registered = /* @__PURE__ */ new Map();
1454
1597
  const registeredChannels = /* @__PURE__ */ new Map();
1455
1598
  const registeredFormatters = /* @__PURE__ */ new Map();
1599
+ const registeredIntake = /* @__PURE__ */ new Map();
1456
1600
  await loaded.activate({
1457
1601
  extensionId: this.options.manifest.id,
1602
+ chatWidgets: { register: (id2, render) => {
1603
+ if (!this.activating || this.options.manifest.apiVersion !== 5 || !this.options.manifest.contributes.chatWidgets?.some((w) => w.id === id2) || registeredWidgets.has(id2) || typeof render !== "function") throw new Error("invalid widget registration");
1604
+ registeredWidgets.set(id2, render);
1605
+ } },
1458
1606
  channels: {
1459
1607
  register: (channelId, handler) => {
1460
1608
  if (!this.activating || this.options.manifest.channelApiVersion !== 1 || !this.options.manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof handler !== "function" || registeredChannels.has(channelId)) {
@@ -1467,6 +1615,12 @@ class ExtensionHostRuntime {
1467
1615
  throw { code: "INVALID_CHANNEL_REGISTRATION", message: "formatter must be declared and registered once" };
1468
1616
  }
1469
1617
  registeredFormatters.set(channelId, formatter);
1618
+ },
1619
+ onIntake: (channelId, handler) => {
1620
+ if (!this.activating || this.options.manifest.channelApiVersion !== 1 || !this.options.manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof handler !== "function" || registeredIntake.has(channelId)) {
1621
+ throw { code: "INVALID_CHANNEL_REGISTRATION", message: "intake handler must be declared and registered once" };
1622
+ }
1623
+ registeredIntake.set(channelId, handler);
1470
1624
  }
1471
1625
  },
1472
1626
  commands: {
@@ -1482,9 +1636,12 @@ class ExtensionHostRuntime {
1482
1636
  throw { code: "HOST_DISPOSED", message: "extension host is disposed" };
1483
1637
  }
1484
1638
  this.module = loaded;
1639
+ for (const [id2, render] of registeredWidgets) this.widgets.set(id2, render);
1485
1640
  this.channelHandlers.clear();
1486
1641
  this.channelFormatters.clear();
1487
1642
  for (const [id2, formatter] of registeredFormatters) this.channelFormatters.set(id2, formatter);
1643
+ this.channelIntakeHandlers.clear();
1644
+ for (const [id2, handler] of registeredIntake) this.channelIntakeHandlers.set(id2, handler);
1488
1645
  for (const [id2, handler] of registeredChannels) this.channelHandlers.set(id2, handler);
1489
1646
  this.commandHandlers.clear();
1490
1647
  for (const [commandId, handler] of registered) this.commandHandlers.set(commandId, handler);