Переглянути джерело

feat: 完成大部分操作

claude-code-best 3 тижнів тому
батько
коміт
50086d2fbb

+ 106 - 0
README.md

@@ -0,0 +1,106 @@
+# Claude Code (Reverse-Engineered)
+
+Anthropic 官方 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI 工具的源码反编译/逆向还原项目。目标是将 Claude Code 核心功能跑通,必要时删减次级能力。
+
+## 核心能力
+
+- API 通信(Anthropic SDK / Bedrock / Vertex)
+- Bash / FileRead / FileWrite / FileEdit 等核心工具
+- REPL 交互界面(ink 终端渲染)
+- 对话历史与会话管理
+- 权限系统
+- Agent / 子代理系统
+
+## 已删减模块
+
+| 模块 | 处理方式 |
+|------|----------|
+| Computer Use (`@ant/computer-use-*`) | stub |
+| Claude for Chrome MCP | stub |
+| Magic Docs / Voice Mode / LSP Server | 移除 |
+| Analytics / GrowthBook / Sentry | 空实现 |
+| Plugins / Marketplace / Desktop Upsell | 移除 |
+| Ultraplan / Tungsten / Auto Dream | 移除 |
+| MCP OAuth/IDP | 简化 |
+| DAEMON / BRIDGE / BG_SESSIONS / TEMPLATES 等 | feature flag 关闭 |
+
+## 快速开始
+
+### 环境要求
+
+- [Bun](https://bun.sh/) >= 1.0
+- Node.js >= 18(部分依赖需要)
+- 有效的 Anthropic API Key(或 Bedrock / Vertex 凭据)
+
+### 安装
+
+```bash
+bun install
+```
+
+### 运行
+
+```bash
+# 开发模式(watch)
+bun run dev
+
+# 直接运行
+bun run src/entrypoints/cli.tsx
+
+# 管道模式(-p)
+echo "say hello" | bun run src/entrypoints/cli.tsx -p
+
+# 构建
+bun run build
+```
+
+构建产物输出到 `dist/cli.js`(~25 MB,5300+ 模块)。
+
+## 项目结构
+
+```
+claude-code/
+├── src/
+│   ├── entrypoints/
+│   │   ├── cli.tsx          # 入口文件(含 MACRO/feature polyfill)
+│   │   └── sdk/             # SDK 子模块 stub
+│   ├── main.tsx             # 主 CLI 逻辑(Commander 定义)
+│   └── types/
+│       ├── global.d.ts      # 全局变量/宏声明
+│       └── internal-modules.d.ts  # 内部 npm 包类型声明
+├── packages/                # Monorepo workspace 包
+│   ├── color-diff-napi/     # 完整实现(终端 color diff)
+│   ├── modifiers-napi/      # stub(macOS 修饰键检测)
+│   ├── audio-capture-napi/  # stub
+│   ├── image-processor-napi/# stub
+│   ├── url-handler-napi/    # stub
+│   └── @ant/               # Anthropic 内部包 stub
+│       ├── claude-for-chrome-mcp/
+│       ├── computer-use-mcp/
+│       ├── computer-use-input/
+│       └── computer-use-swift/
+├── scripts/                 # 自动化 stub 生成脚本
+├── dist/                    # 构建输出
+└── package.json             # Bun workspaces monorepo 配置
+```
+
+## 技术说明
+
+### 运行时 Polyfill
+
+入口文件 `src/entrypoints/cli.tsx` 顶部注入了必要的 polyfill:
+
+- `feature()` — 所有 feature flag 返回 `false`,跳过未实现分支
+- `globalThis.MACRO` — 模拟构建时宏注入(VERSION 等)
+
+### 类型状态
+
+仍有 ~1341 个 tsc 错误,均为反编译产生的源码级类型问题(`unknown` / `never` / `{}`),**不影响 Bun 运行时**。
+
+### Monorepo
+
+项目采用 Bun workspaces 管理内部包。原先手工放在 `node_modules/` 下的 stub 已统一迁入 `packages/`,通过 `workspace:*` 解析。
+
+## 许可证
+
+本项目仅供学习研究用途。Claude Code 的所有权利归 [Anthropic](https://www.anthropic.com/) 所有。

+ 54 - 0
RECORD.md

@@ -103,3 +103,57 @@ const feature = (_name: string) => false;  // 所有 feature flag 分支被跳
 | `scripts/create-type-stubs.mjs` | 自动 stub 生成脚本 |
 | `scripts/fix-default-stubs.mjs` | 修复默认导出 stub |
 | `scripts/fix-missing-exports.mjs` | 补全缺失导出 |
+
+---
+
+## 五、Monorepo 改造(2026-03-31)
+
+### 5.1 背景
+
+`color-diff-napi` 原先是手工放在 `node_modules/` 下的 stub 文件,导出的是普通对象而非 class,导致 `new ColorDiff(...)` 报错:
+```
+ERROR Object is not a constructor (evaluating 'new ColorDiff(patch, firstLine, filePath, fileContent)')
+```
+同时 `@ant/*`、其他 `*-napi` 包也只有 `declare module` 类型声明,无运行时实现。
+
+### 5.2 方案
+
+将项目改造为 **Bun workspaces monorepo**,所有内部包统一放在 `packages/` 下,通过 `workspace:*` 依赖解析。
+
+### 5.3 创建的 workspace 包
+
+| 包名 | 路径 | 类型 |
+|------|------|------|
+| `color-diff-napi` | `packages/color-diff-napi/` | 完整实现(~1000行 TS,从 `src/native-ts/color-diff/` 移入) |
+| `modifiers-napi` | `packages/modifiers-napi/` | stub(macOS 修饰键检测) |
+| `audio-capture-napi` | `packages/audio-capture-napi/` | stub |
+| `image-processor-napi` | `packages/image-processor-napi/` | stub |
+| `url-handler-napi` | `packages/url-handler-napi/` | stub |
+| `@ant/claude-for-chrome-mcp` | `packages/@ant/claude-for-chrome-mcp/` | stub |
+| `@ant/computer-use-mcp` | `packages/@ant/computer-use-mcp/` | stub(含 subpath exports: sentinelApps, types) |
+| `@ant/computer-use-input` | `packages/@ant/computer-use-input/` | stub |
+| `@ant/computer-use-swift` | `packages/@ant/computer-use-swift/` | stub |
+
+### 5.4 新增的 npm 依赖
+
+| 包名 | 原因 |
+|------|------|
+| `@opentelemetry/semantic-conventions` | 构建报错缺失 |
+| `fflate` | `src/utils/dxt/zip.ts` 动态 import |
+| `vscode-jsonrpc` | `src/services/lsp/LSPClient.ts` import |
+| `@aws-sdk/credential-provider-node` | `src/utils/proxy.ts` 动态 import |
+
+### 5.5 关键变更
+
+- `package.json`:添加 `workspaces`,添加所有 workspace 包和缺失 npm 依赖
+- `src/types/internal-modules.d.ts`:删除已移入 monorepo 的 `declare module` 块,仅保留 `bun:bundle`、`bun:ffi`、`@anthropic-ai/mcpb`
+- `src/native-ts/color-diff/` → `packages/color-diff-napi/src/`:移动并内联了对 `stringWidth` 和 `logError` 的依赖
+- 删除 `node_modules/color-diff-napi/` 手工 stub
+
+### 5.6 构建验证
+
+```
+$ bun run build
+Bundled 5326 modules in 491ms
+  cli.js  25.74 MB  (entry point)
+```

+ 1 - 1
package.json

@@ -9,7 +9,7 @@
     ],
     "scripts": {
         "build": "bun build src/entrypoints/cli.tsx --outdir dist --target bun",
-        "dev": "bun run --watch src/entrypoints/cli.tsx"
+        "dev": "bun run src/entrypoints/cli.tsx"
     },
     "dependencies": {
         "@alcalzone/ansi-tokenize": "^0.3.0",

+ 44 - 2
packages/@ant/computer-use-input/src/index.ts

@@ -1,2 +1,44 @@
-export class ComputerUseInput {}
-export class ComputerUseInputAPI {}
+interface FrontmostAppInfo {
+  bundleId: string
+  appName: string
+}
+
+export class ComputerUseInputAPI {
+  declare moveMouse: (
+    x: number,
+    y: number,
+    animated: boolean,
+  ) => Promise<void>
+
+  declare key: (
+    key: string,
+    action: 'press' | 'release',
+  ) => Promise<void>
+
+  declare keys: (parts: string[]) => Promise<void>
+
+  declare mouseLocation: () => Promise<{ x: number; y: number }>
+
+  declare mouseButton: (
+    button: 'left' | 'right' | 'middle',
+    action: 'click' | 'press' | 'release',
+    count?: number,
+  ) => Promise<void>
+
+  declare mouseScroll: (
+    amount: number,
+    direction: 'vertical' | 'horizontal',
+  ) => Promise<void>
+
+  declare typeText: (text: string) => Promise<void>
+
+  declare getFrontmostAppInfo: () => FrontmostAppInfo | null
+
+  declare isSupported: true
+}
+
+interface ComputerUseInputUnsupported {
+  isSupported: false
+}
+
+export type ComputerUseInput = ComputerUseInputAPI | ComputerUseInputUnsupported

+ 112 - 1
packages/@ant/computer-use-swift/src/index.ts

@@ -1 +1,112 @@
-export class ComputerUseAPI {}
+interface DisplayGeometry {
+  width: number
+  height: number
+  scaleFactor: number
+  displayId: number
+}
+
+interface PrepareDisplayResult {
+  activated: string
+  hidden: string[]
+}
+
+interface AppInfo {
+  bundleId: string
+  displayName: string
+}
+
+interface InstalledApp {
+  bundleId: string
+  displayName: string
+  path: string
+  iconDataUrl?: string
+}
+
+interface RunningApp {
+  bundleId: string
+  displayName: string
+}
+
+interface ScreenshotResult {
+  base64: string
+  width: number
+  height: number
+}
+
+interface ResolvePrepareCaptureResult {
+  base64: string
+  width: number
+  height: number
+}
+
+interface WindowDisplayInfo {
+  bundleId: string
+  displayIds: number[]
+}
+
+interface AppsAPI {
+  prepareDisplay(
+    allowlistBundleIds: string[],
+    surrogateHost: string,
+    displayId?: number,
+  ): Promise<PrepareDisplayResult>
+  previewHideSet(
+    bundleIds: string[],
+    displayId?: number,
+  ): Promise<Array<AppInfo>>
+  findWindowDisplays(
+    bundleIds: string[],
+  ): Promise<Array<WindowDisplayInfo>>
+  appUnderPoint(
+    x: number,
+    y: number,
+  ): Promise<AppInfo | null>
+  listInstalled(): Promise<InstalledApp[]>
+  iconDataUrl(path: string): string | null
+  listRunning(): RunningApp[]
+  open(bundleId: string): Promise<void>
+  unhide(bundleIds: string[]): Promise<void>
+}
+
+interface DisplayAPI {
+  getSize(displayId?: number): DisplayGeometry
+  listAll(): DisplayGeometry[]
+}
+
+interface ScreenshotAPI {
+  captureExcluding(
+    allowedBundleIds: string[],
+    quality: number,
+    targetW: number,
+    targetH: number,
+    displayId?: number,
+  ): Promise<ScreenshotResult>
+  captureRegion(
+    allowedBundleIds: string[],
+    x: number,
+    y: number,
+    w: number,
+    h: number,
+    outW: number,
+    outH: number,
+    quality: number,
+    displayId?: number,
+  ): Promise<ScreenshotResult>
+}
+
+export class ComputerUseAPI {
+  declare apps: AppsAPI
+  declare display: DisplayAPI
+  declare screenshot: ScreenshotAPI
+
+  declare resolvePrepareCapture: (
+    allowedBundleIds: string[],
+    surrogateHost: string,
+    quality: number,
+    targetW: number,
+    targetH: number,
+    displayId?: number,
+    autoResolve?: boolean,
+    doHide?: boolean,
+  ) => Promise<ResolvePrepareCaptureResult>
+}

+ 8 - 1
src/components/FeedbackSurvey/useFrustrationDetection.ts

@@ -1,2 +1,9 @@
 // Auto-generated stub — replace with real implementation
-export {};
+export function useFrustrationDetection(
+  _messages: unknown[],
+  _isLoading: boolean,
+  _hasActivePrompt: boolean,
+  _otherSurveyOpen: boolean,
+): { state: 'closed' | 'open'; handleTranscriptSelect: () => void } {
+  return { state: 'closed', handleTranscriptSelect: () => {} };
+}

+ 9 - 9
src/components/PromptInput/PromptInput.tsx

@@ -294,8 +294,8 @@ function PromptInput({
   // otherwise bridge becomes an invisible selection stop.
   const bridgeFooterVisible = replBridgeConnected && (replBridgeExplicit || replBridgeReconnecting);
   // Tmux pill (ant-only) — visible when there's an active tungsten session
-  const hasTungstenSession = useAppState(s => "external" === 'ant' && s.tungstenActiveSession !== undefined);
-  const tmuxFooterVisible = "external" === 'ant' && hasTungstenSession;
+  const hasTungstenSession = useAppState(s => ("external" as string) === 'ant' && s.tungstenActiveSession !== undefined);
+  const tmuxFooterVisible = ("external" as string) === 'ant' && hasTungstenSession;
   // WebBrowser pill — visible when a browser is open
   const bagelFooterVisible = useAppState(s => false);
   const teamContext = useAppState(s => s.teamContext);
@@ -391,7 +391,7 @@ function PromptInput({
   // exist. When only local_agent tasks are running (coordinator/fork mode), the
   // pill is absent, so the -1 sentinel would leave nothing visually selected.
   // In that case, skip -1 and treat 0 as the minimum selectable index.
-  const hasBgTaskPill = useMemo(() => Object.values(tasks).some(t => isBackgroundTask(t) && !("external" === 'ant' && isPanelAgentTask(t))), [tasks]);
+  const hasBgTaskPill = useMemo(() => Object.values(tasks).some(t => isBackgroundTask(t) && !(("external" as string) === 'ant' && isPanelAgentTask(t))), [tasks]);
   const minCoordinatorIndex = hasBgTaskPill ? -1 : 0;
   // Clamp index when tasks complete and the list shrinks beneath the cursor
   useEffect(() => {
@@ -455,7 +455,7 @@ function PromptInput({
   // Panel shows retained-completed agents too (getVisibleAgentTasks), so the
   // pill must stay navigable whenever the panel has rows — not just when
   // something is running.
-  const tasksFooterVisible = (runningTaskCount > 0 || "external" === 'ant' && coordinatorTaskCount > 0) && !shouldHideTasksFooter(tasks, showSpinnerTree);
+  const tasksFooterVisible = (runningTaskCount > 0 || ("external" as string) === 'ant' && coordinatorTaskCount > 0) && !shouldHideTasksFooter(tasks, showSpinnerTree);
   const teamsFooterVisible = cachedTeams.length > 0;
   const footerItems = useMemo(() => [tasksFooterVisible && 'tasks', tmuxFooterVisible && 'tmux', bagelFooterVisible && 'bagel', teamsFooterVisible && 'teams', bridgeFooterVisible && 'bridge', companionFooterVisible && 'companion'].filter(Boolean) as FooterItem[], [tasksFooterVisible, tmuxFooterVisible, bagelFooterVisible, teamsFooterVisible, bridgeFooterVisible, companionFooterVisible]);
 
@@ -1054,7 +1054,7 @@ function PromptInput({
           clearBuffer();
           resetHistory();
           return;
-        } else if (result.error === 'no_team_context') {
+        } else if ('error' in result && result.error === 'no_team_context') {
           // No team context - fall through to normal prompt submission
         } else {
           // Unknown recipient - fall through to normal prompt submission
@@ -1742,7 +1742,7 @@ function PromptInput({
   useKeybindings({
     'footer:up': () => {
       // ↑ scrolls within the coordinator task list before leaving the pill
-      if (tasksSelected && "external" === 'ant' && coordinatorTaskCount > 0 && coordinatorTaskIndex > minCoordinatorIndex) {
+      if (tasksSelected && ("external" as string) === 'ant' && coordinatorTaskCount > 0 && coordinatorTaskIndex > minCoordinatorIndex) {
         setCoordinatorTaskIndex(prev => prev - 1);
         return;
       }
@@ -1750,7 +1750,7 @@ function PromptInput({
     },
     'footer:down': () => {
       // ↓ scrolls within the coordinator task list, never leaves the pill
-      if (tasksSelected && "external" === 'ant' && coordinatorTaskCount > 0) {
+      if (tasksSelected && ("external" as string) === 'ant' && coordinatorTaskCount > 0) {
         if (coordinatorTaskIndex < coordinatorTaskCount - 1) {
           setCoordinatorTaskIndex(prev => prev + 1);
         }
@@ -1813,7 +1813,7 @@ function PromptInput({
           }
           break;
         case 'tmux':
-          if ("external" === 'ant') {
+          if (("external" as string) === 'ant') {
             setAppState(prev => prev.tungstenPanelAutoHidden ? {
               ...prev,
               tungstenPanelAutoHidden: false
@@ -2306,7 +2306,7 @@ function getInitialPasteId(messages: Message[]): number {
     if (message.type === 'user') {
       // Check image paste IDs
       if (message.imagePasteIds) {
-        for (const id of message.imagePasteIds) {
+        for (const id of message.imagePasteIds as number[]) {
           if (id > maxId) maxId = id;
         }
       }

+ 2 - 2
src/components/PromptInput/PromptInputFooter.tsx

@@ -143,11 +143,11 @@ function PromptInputFooter({
         </Box>
         <Box flexShrink={1} gap={1}>
           {isFullscreen ? null : <Notifications apiKeyStatus={apiKeyStatus} autoUpdaterResult={autoUpdaterResult} debug={debug} isAutoUpdating={isAutoUpdating} verbose={verbose} messages={messages} onAutoUpdaterResult={onAutoUpdaterResult} onChangeIsUpdating={onChangeIsUpdating} ideSelection={ideSelection} mcpClients={mcpClients} isInputWrapped={isInputWrapped} isNarrow={isNarrow} />}
-          {"external" === 'ant' && isUndercover() && <Text dimColor>undercover</Text>}
+          {("external" as string) === 'ant' && isUndercover() && <Text dimColor>undercover</Text>}
           <BridgeStatusIndicator bridgeSelected={bridgeSelected} />
         </Box>
       </Box>
-      {"external" === 'ant' && <CoordinatorTaskPanel />}
+      {("external" as string) === 'ant' && <CoordinatorTaskPanel />}
     </>;
 }
 export default memo(PromptInputFooter);

+ 4 - 4
src/components/PromptInput/PromptInputFooterLeftSide.tsx

@@ -260,7 +260,7 @@ function ModeIndicator({
   const expandedView = useAppState(s_3 => s_3.expandedView);
   const showSpinnerTree = expandedView === 'teammates';
   const prStatus = usePrStatus(isLoading, isPrStatusEnabled());
-  const hasTmuxSession = useAppState(s_4 => "external" === 'ant' && s_4.tungstenActiveSession !== undefined);
+  const hasTmuxSession = useAppState(s_4 => ("external" as string) === 'ant' && s_4.tungstenActiveSession !== undefined);
   const nextTickAt = useSyncExternalStore(proactiveModule?.subscribeToProactiveChanges ?? NO_OP_SUBSCRIBE, proactiveModule?.getNextTickAt ?? NULL, NULL);
   // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
   const voiceEnabled = feature('VOICE_MODE') ? useVoiceEnabled() : false;
@@ -274,7 +274,7 @@ function ModeIndicator({
   const selGetState = useSelection().getState;
   const hasNextTick = nextTickAt !== null;
   const isCoordinator = feature('COORDINATOR_MODE') ? coordinatorModule?.isCoordinatorMode() === true : false;
-  const runningTaskCount = useMemo(() => count(Object.values(tasks), t => isBackgroundTask(t) && !("external" === 'ant' && isPanelAgentTask(t))), [tasks]);
+  const runningTaskCount = useMemo(() => count(Object.values(tasks), t => isBackgroundTask(t) && !(("external" as string) === 'ant' && isPanelAgentTask(t))), [tasks]);
   const tasksV2 = useTasksV2();
   const hasTaskItems = tasksV2 !== undefined && tasksV2.length > 0;
   const escShortcut = useShortcutDisplay('chat:cancel', 'Chat', 'esc').toLowerCase();
@@ -365,7 +365,7 @@ function ModeIndicator({
   // its click-target Box isn't nested inside the <Text wrap="truncate">
   // wrapper (reconciler throws on Box-in-Text).
   // Tmux pill (ant-only) — appears right after tasks in nav order
-  ...("external" === 'ant' && hasTmuxSession ? [<TungstenPill key="tmux" selected={tmuxSelected} />] : []), ...(isAgentSwarmsEnabled() && hasTeams ? [<TeamStatus key="teams" teamsSelected={teamsSelected} showHint={showHint && !hasBackgroundTasks} />] : []), ...(shouldShowPrStatus ? [<PrBadge key="pr-status" number={prStatus.number!} url={prStatus.url!} reviewState={prStatus.reviewState!} />] : [])];
+  ...(("external" as string) === 'ant' && hasTmuxSession ? [<TungstenPill key="tmux" selected={tmuxSelected} />] : []), ...(isAgentSwarmsEnabled() && hasTeams ? [<TeamStatus key="teams" teamsSelected={teamsSelected} showHint={showHint && !hasBackgroundTasks} />] : []), ...(shouldShowPrStatus ? [<PrBadge key="pr-status" number={prStatus.number!} url={prStatus.url!} reviewState={prStatus.reviewState!} />] : [])];
 
   // Check if any in-process teammates exist (for hint text cycling)
   const hasAnyInProcessTeammates = Object.values(tasks).some(t_2 => t_2.type === 'in_process_teammate' && t_2.status === 'running');
@@ -399,7 +399,7 @@ function ModeIndicator({
   }
 
   // Add "↓ to manage tasks" hint when panel has visible rows
-  const hasCoordinatorTasks = "external" === 'ant' && getVisibleAgentTasks(tasks).length > 0;
+  const hasCoordinatorTasks = ("external" as string) === 'ant' && getVisibleAgentTasks(tasks).length > 0;
 
   // Tasks pill renders as a Box sibling (not a parts entry) so its
   // click-target Box isn't nested inside <Text wrap="truncate"> — the

+ 1 - 1
src/components/PromptInput/PromptInputFooterSuggestions.tsx

@@ -34,7 +34,7 @@ function getIcon(itemId: string): string {
 function isUnifiedSuggestion(itemId: string): boolean {
   return itemId.startsWith('file-') || itemId.startsWith('mcp-resource-') || itemId.startsWith('agent-');
 }
-const SuggestionItemRow = memo(function SuggestionItemRow(t0) {
+const SuggestionItemRow = memo(function SuggestionItemRow(t0: { item: SuggestionItem; maxColumnWidth: number; isSelected: boolean }) {
   const $ = _c(36);
   const {
     item,

+ 2 - 2
src/components/PromptInput/ShimmeredInput.tsx

@@ -74,8 +74,8 @@ export function HighlightedInput(t0) {
       $[8] = lo;
       $[9] = hi;
     } else {
-      lo = $[8];
-      hi = $[9];
+      lo = $[8] as number;
+      hi = $[9] as number;
     }
     sweepStart = lo - 10;
     cycleLength = hi - lo + 20;

+ 4 - 0
src/entrypoints/cli.tsx

@@ -11,6 +11,10 @@ if (typeof globalThis.MACRO === "undefined") {
         VERSION_CHANGELOG: "",
     };
 }
+// Build-time constants — normally replaced by Bun bundler at compile time
+(globalThis as any).BUILD_TARGET = "external";
+(globalThis as any).BUILD_ENV = "production";
+(globalThis as any).INTERFACE_TYPE = "stdio";
 
 // Bugfix for corepack auto-pinning, which adds yarnpkg to peoples' package.jsons
 // eslint-disable-next-line custom-rules/no-top-level-side-effects

+ 2 - 1
src/entrypoints/sdk/coreTypes.generated.ts

@@ -98,7 +98,8 @@ export type SDKMessage = { type: string; [key: string]: unknown }
 export type SDKUserMessage = { type: "user"; content: unknown; uuid: string; [key: string]: unknown }
 export type SDKUserMessageReplay = SDKUserMessage
 export type SDKAssistantMessage = { type: "assistant"; content: unknown; [key: string]: unknown }
-export type SDKAssistantMessageError = { type: "assistant_error"; error: unknown; [key: string]: unknown }
+export type SDKAssistantErrorMessage = { type: "assistant_error"; error: unknown; [key: string]: unknown }
+export type SDKAssistantMessageError = 'authentication_failed' | 'billing_error' | 'rate_limit' | 'invalid_request' | 'server_error' | 'unknown' | 'max_output_tokens'
 export type SDKPartialAssistantMessage = { type: "partial_assistant"; [key: string]: unknown }
 export type SDKResultMessage = { type: "result"; [key: string]: unknown }
 export type SDKResultSuccess = { type: "result_success"; [key: string]: unknown }

+ 1 - 1
src/hooks/notifs/useAntOrgWarningNotification.ts

@@ -1,2 +1,2 @@
 // Auto-generated stub — replace with real implementation
-export {};
+export function useAntOrgWarningNotification(): void {}

+ 25 - 25
src/screens/REPL.tsx

@@ -104,13 +104,13 @@ const VoiceKeybindingHandler: typeof import('../hooks/useVoiceIntegration.js').V
 // Frustration detection is ant-only (dogfooding). Conditional require so external
 // builds eliminate the module entirely (including its two O(n) useMemos that run
 // on every messages change, plus the GrowthBook fetch).
-const useFrustrationDetection: typeof import('../components/FeedbackSurvey/useFrustrationDetection.js').useFrustrationDetection = "external" === 'ant' ? require('../components/FeedbackSurvey/useFrustrationDetection.js').useFrustrationDetection : () => ({
+const useFrustrationDetection: typeof import('../components/FeedbackSurvey/useFrustrationDetection.js').useFrustrationDetection = ("external" as string) === 'ant' ? require('../components/FeedbackSurvey/useFrustrationDetection.js').useFrustrationDetection : () => ({
   state: 'closed',
   handleTranscriptSelect: () => {}
 });
 // Ant-only org warning. Conditional require so the org UUID list is
 // eliminated from external builds (one UUID is on excluded-strings).
-const useAntOrgWarningNotification: typeof import('../hooks/notifs/useAntOrgWarningNotification.js').useAntOrgWarningNotification = "external" === 'ant' ? require('../hooks/notifs/useAntOrgWarningNotification.js').useAntOrgWarningNotification : () => {};
+const useAntOrgWarningNotification: typeof import('../hooks/notifs/useAntOrgWarningNotification.js').useAntOrgWarningNotification = ("external" as string) === 'ant' ? require('../hooks/notifs/useAntOrgWarningNotification.js').useAntOrgWarningNotification : () => {};
 // Dead code elimination: conditional import for coordinator mode
 const getCoordinatorUserContext: (mcpClients: ReadonlyArray<{
   name: string;
@@ -218,9 +218,9 @@ import { EffortCallout, shouldShowEffortCallout } from '../components/EffortCall
 import type { EffortValue } from '../utils/effort.js';
 import { RemoteCallout } from '../components/RemoteCallout.js';
 /* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
-const AntModelSwitchCallout = "external" === 'ant' ? require('../components/AntModelSwitchCallout.js').AntModelSwitchCallout : null;
-const shouldShowAntModelSwitch = "external" === 'ant' ? require('../components/AntModelSwitchCallout.js').shouldShowModelSwitchCallout : (): boolean => false;
-const UndercoverAutoCallout = "external" === 'ant' ? require('../components/UndercoverAutoCallout.js').UndercoverAutoCallout : null;
+const AntModelSwitchCallout = ("external" as string) === 'ant' ? require('../components/AntModelSwitchCallout.js').AntModelSwitchCallout : null;
+const shouldShowAntModelSwitch = ("external" as string) === 'ant' ? require('../components/AntModelSwitchCallout.js').shouldShowModelSwitchCallout : (): boolean => false;
+const UndercoverAutoCallout = ("external" as string) === 'ant' ? require('../components/UndercoverAutoCallout.js').UndercoverAutoCallout : null;
 /* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
 import { activityManager } from '../utils/activityManager.js';
 import { createAbortController } from '../utils/abortController.js';
@@ -601,7 +601,7 @@ export function REPL({
   // Env-var gates hoisted to mount-time — isEnvTruthy does toLowerCase+trim+
   // includes, and these were on the render path (hot during PageUp spam).
   const titleDisabled = useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_TERMINAL_TITLE), []);
-  const moreRightEnabled = useMemo(() => "external" === 'ant' && isEnvTruthy(process.env.CLAUDE_MORERIGHT), []);
+  const moreRightEnabled = useMemo(() => ("external" as string) === 'ant' && isEnvTruthy(process.env.CLAUDE_MORERIGHT), []);
   const disableVirtualScroll = useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_VIRTUAL_SCROLL), []);
   const disableMessageActions = feature('MESSAGE_ACTIONS') ?
   // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
@@ -733,7 +733,7 @@ export function REPL({
   const [showIdeOnboarding, setShowIdeOnboarding] = useState(false);
   // Dead code elimination: model switch callout state (ant-only)
   const [showModelSwitchCallout, setShowModelSwitchCallout] = useState(() => {
-    if ("external" === 'ant') {
+    if (("external" as string) === 'ant') {
       return shouldShowAntModelSwitch();
     }
     return false;
@@ -1012,7 +1012,7 @@ export function REPL({
   }, []);
   const [showUndercoverCallout, setShowUndercoverCallout] = useState(false);
   useEffect(() => {
-    if ("external" === 'ant') {
+    if (("external" as string) === 'ant') {
       void (async () => {
         // Wait for repo classification to settle (memoized, no-op if primed).
         const {
@@ -2041,10 +2041,10 @@ export function REPL({
     if (allowDialogsWithAnimation && showIdeOnboarding) return 'ide-onboarding';
 
     // Model switch callout (ant-only, eliminated from external builds)
-    if ("external" === 'ant' && allowDialogsWithAnimation && showModelSwitchCallout) return 'model-switch';
+    if (("external" as string) === 'ant' && allowDialogsWithAnimation && showModelSwitchCallout) return 'model-switch';
 
     // Undercover auto-enable explainer (ant-only, eliminated from external builds)
-    if ("external" === 'ant' && allowDialogsWithAnimation && showUndercoverCallout) return 'undercover-callout';
+    if (("external" as string) === 'ant' && allowDialogsWithAnimation && showUndercoverCallout) return 'undercover-callout';
 
     // Effort callout (shown once for Opus 4.6 users when effort is enabled)
     if (allowDialogsWithAnimation && showEffortCallout) return 'effort-callout';
@@ -2482,7 +2482,7 @@ export function REPL({
       dynamicSkillDirTriggers: new Set<string>(),
       discoveredSkillNames: discoveredSkillNamesRef.current,
       setResponseLength,
-      pushApiMetricsEntry: "external" === 'ant' ? (ttftMs: number) => {
+      pushApiMetricsEntry: ("external" as string) === 'ant' ? (ttftMs: number) => {
         const now = Date.now();
         const baseline = responseLengthRef.current;
         apiMetricsRef.current.push({
@@ -2605,7 +2605,7 @@ export function REPL({
         if (feature('PROACTIVE') || feature('KAIROS')) {
           proactiveModule?.setContextBlocked(false);
         }
-      } else if (newMessage.type === 'progress' && isEphemeralToolProgress(newMessage.data.type)) {
+      } else if ((newMessage as MessageType).type === 'progress' && isEphemeralToolProgress((newMessage as ProgressMessage<unknown>).data.type)) {
         // Replace the previous ephemeral progress tick for the same tool
         // call instead of appending. Sleep/Bash emit a tick per second and
         // only the last one is rendered; appending blows up the messages
@@ -2618,7 +2618,7 @@ export function REPL({
         // "Initializing…" because it renders the full progress trail.
         setMessages(oldMessages => {
           const last = oldMessages.at(-1);
-          if (last?.type === 'progress' && last.parentToolUseID === newMessage.parentToolUseID && last.data.type === newMessage.data.type) {
+          if (last?.type === 'progress' && last.parentToolUseID === (newMessage as MessageType).parentToolUseID && last.data.type === (newMessage as ProgressMessage<unknown>).data.type) {
             const copy = oldMessages.slice();
             copy[copy.length - 1] = newMessage;
             return copy;
@@ -2804,14 +2804,14 @@ export function REPL({
     if (feature('BUDDY')) {
       void fireCompanionObserver(messagesRef.current, reaction => setAppState(prev => prev.companionReaction === reaction ? prev : {
         ...prev,
-        companionReaction: reaction
+        companionReaction: reaction as string | undefined
       }));
     }
     queryCheckpoint('query_end');
 
     // Capture ant-only API metrics before resetLoadingState clears the ref.
     // For multi-request turns (tool use loops), compute P50 across all requests.
-    if ("external" === 'ant' && apiMetricsRef.current.length > 0) {
+    if (("external" as string) === 'ant' && apiMetricsRef.current.length > 0) {
       const entries = apiMetricsRef.current;
       const ttfts = entries.map(e => e.ttftMs);
       // Compute per-request OTPS using only active streaming time and
@@ -2939,7 +2939,7 @@ export function REPL({
         // minutes — wiping the session made the pill disappear entirely, forcing
         // the user to re-invoke Tmux just to peek. Skip on abort so the panel
         // stays open for inspection (matches the turn-duration guard below).
-        if ("external" === 'ant' && !abortController.signal.aborted) {
+        if (("external" as string) === 'ant' && !abortController.signal.aborted) {
           setAppState(prev => {
             if (prev.tungstenActiveSession === undefined) return prev;
             if (prev.tungstenPanelAutoHidden === true) return prev;
@@ -3062,7 +3062,7 @@ export function REPL({
       }
 
       // Atomically: clear initial message, set permission mode and rules, and store plan for verification
-      const shouldStorePlanForVerification = initialMsg.message.planContent && "external" === 'ant' && isEnvTruthy(undefined);
+      const shouldStorePlanForVerification = initialMsg.message.planContent && ("external" as string) === 'ant' && isEnvTruthy(undefined);
       setAppState(prev => {
         // Build and apply permission updates (mode + allowedPrompts rules)
         let updatedToolPermissionContext = initialMsg.mode ? applyPermissionUpdates(prev.toolPermissionContext, buildPermissionUpdates(initialMsg.mode, initialMsg.allowedPrompts)) : prev.toolPermissionContext;
@@ -3595,7 +3595,7 @@ export function REPL({
 
   // Handler for when user presses 1 on survey thanks screen to share details
   const handleSurveyRequestFeedback = useCallback(() => {
-    const command = "external" === 'ant' ? '/issue' : '/feedback';
+    const command = ("external" as string) === 'ant' ? '/issue' : '/feedback';
     onSubmit(command, {
       setCursorOffset: () => {},
       clearBuffer: () => {},
@@ -4063,7 +4063,7 @@ export function REPL({
   // - Workers receive permission responses via mailbox messages
   // - Leaders receive permission requests via mailbox messages
 
-  if ("external" === 'ant') {
+  if (("external" as string) === 'ant') {
     // Tasks mode: watch for tasks and auto-process them
     // eslint-disable-next-line react-hooks/rules-of-hooks
     // biome-ignore lint/correctness/useHookAtTopLevel: conditional for dead code elimination in external builds
@@ -4172,7 +4172,7 @@ export function REPL({
 
     // Fall back to default behavior
     const hookType = currentHooks[0]?.data.hookEvent === 'SubagentStop' ? 'subagent stop' : 'stop';
-    if ("external" === 'ant') {
+    if (("external" as string) === 'ant') {
       const cmd = currentHooks[completedCount]?.data.command;
       const label = cmd ? ` '${truncateToWidth(cmd, 40)}'` : '';
       return total === 1 ? `running ${hookType} hook${label}` : `running ${hookType} hook${label}\u2026 ${completedCount}/${total}`;
@@ -4581,7 +4581,7 @@ export function REPL({
               {toolJSX && !(toolJSX.isLocalJSXCommand && toolJSX.isImmediate) && !toolJsxCentered && <Box flexDirection="column" width="100%">
                     {toolJSX.jsx}
                   </Box>}
-              {"external" === 'ant' && <TungstenLiveMonitor />}
+              {("external" as string) === 'ant' && <TungstenLiveMonitor />}
               {feature('WEB_BROWSER_TOOL') ? WebBrowserPanelModule && <WebBrowserPanelModule.WebBrowserPanel /> : null}
               <Box flexGrow={1} />
               {showSpinner && <SpinnerWithVerb mode={streamMode} spinnerTip={spinnerTip} responseLengthRef={responseLengthRef} apiMetricsRef={apiMetricsRef} overrideMessage={spinnerMessage} spinnerSuffix={stopHookSpinnerSuffix} verbose={verbose} loadingStartTimeRef={loadingStartTimeRef} totalPausedMsRef={totalPausedMsRef} pauseStartTimeRef={pauseStartTimeRef} overrideColor={spinnerColor} overrideShimmerColor={spinnerShimmerColor} hasActiveTools={inProgressToolUseIDs.size > 0} leaderIsIdle={!isLoading} />}
@@ -4804,7 +4804,7 @@ export function REPL({
             });
           }} />}
                 {focusedInputDialog === 'ide-onboarding' && <IdeOnboardingDialog onDone={() => setShowIdeOnboarding(false)} installationStatus={ideInstallationStatus} />}
-                {"external" === 'ant' && focusedInputDialog === 'model-switch' && AntModelSwitchCallout && <AntModelSwitchCallout onDone={(selection: string, modelAlias?: string) => {
+                {("external" as string) === 'ant' && focusedInputDialog === 'model-switch' && AntModelSwitchCallout && <AntModelSwitchCallout onDone={(selection: string, modelAlias?: string) => {
             setShowModelSwitchCallout(false);
             if (selection === 'switch' && modelAlias) {
               setAppState(prev => ({
@@ -4814,7 +4814,7 @@ export function REPL({
               }));
             }
           }} />}
-                {"external" === 'ant' && focusedInputDialog === 'undercover-callout' && UndercoverAutoCallout && <UndercoverAutoCallout onDone={() => setShowUndercoverCallout(false)} />}
+                {("external" as string) === 'ant' && focusedInputDialog === 'undercover-callout' && UndercoverAutoCallout && <UndercoverAutoCallout onDone={() => setShowUndercoverCallout(false)} />}
                 {focusedInputDialog === 'effort-callout' && <EffortCallout model={mainLoopModel} onDone={selection => {
             setShowEffortCallout(false);
             if (selection !== 'dismiss') {
@@ -4897,7 +4897,7 @@ export function REPL({
                       {/* Frustration-triggered transcript sharing prompt */}
                       {frustrationDetection.state !== 'closed' && <FeedbackSurvey state={frustrationDetection.state} lastResponse={null} handleSelect={() => {}} handleTranscriptSelect={frustrationDetection.handleTranscriptSelect} inputValue={inputValue} setInputValue={setInputValue} />}
                       {/* Skill improvement survey - appears when improvements detected (ant-only) */}
-                      {"external" === 'ant' && skillImprovementSurvey.suggestion && <SkillImprovementSurvey isOpen={skillImprovementSurvey.isOpen} skillName={skillImprovementSurvey.suggestion.skillName} updates={skillImprovementSurvey.suggestion.updates} handleSelect={skillImprovementSurvey.handleSelect} inputValue={inputValue} setInputValue={setInputValue} />}
+                      {("external" as string) === 'ant' && skillImprovementSurvey.suggestion && <SkillImprovementSurvey isOpen={skillImprovementSurvey.isOpen} skillName={skillImprovementSurvey.suggestion.skillName} updates={skillImprovementSurvey.suggestion.updates} handleSelect={skillImprovementSurvey.handleSelect} inputValue={inputValue} setInputValue={setInputValue} />}
                       {showIssueFlagBanner && <IssueFlagBanner />}
                       {}
                       <PromptInput debug={debug} ideSelection={ideSelection} hasSuppressedDialogs={!!hasSuppressedDialogs} isLocalJSXCommandActive={isShowingLocalJSXCommand} getToolUseContext={getToolUseContext} toolPermissionContext={toolPermissionContext} setToolPermissionContext={setToolPermissionContext} apiKeyStatus={apiKeyStatus} commands={commands} agents={agentDefinitions.activeAgents} isLoading={isLoading} onExit={handleExit} verbose={verbose} messages={messages} onAutoUpdaterResult={setAutoUpdaterResult} autoUpdaterResult={autoUpdaterResult} input={inputValue} onInputChange={setInputValue} mode={inputMode} onModeChange={setInputMode} stashedPrompt={stashedPrompt} setStashedPrompt={setStashedPrompt} submitCount={submitCount} onShowMessageSelector={handleShowMessageSelector} onMessageActionsEnter={
@@ -4990,7 +4990,7 @@ export function REPL({
             setIsMessageSelectorVisible(false);
             setMessageSelectorPreselect(undefined);
           }} />}
-                {"external" === 'ant' && <DevBar />}
+                {("external" as string) === 'ant' && <DevBar />}
               </Box>
               {feature('BUDDY') && !(companionNarrow && isFullscreenEnvEnabled()) && companionVisible ? <CompanionSprite /> : null}
             </Box>} />

+ 2 - 2
src/state/AppState.tsx

@@ -139,7 +139,7 @@ function useAppStore(): AppStateStore {
  * const { text, promptId } = useAppState(s => s.promptSuggestion) // good
  * ```
  */
-export function useAppState(selector) {
+export function useAppState<R>(selector: (state: AppState) => R): R {
   const $ = _c(3);
   const store = useAppStore();
   let t0;
@@ -183,7 +183,7 @@ const NOOP_SUBSCRIBE = () => () => {};
  * Safe version of useAppState that returns undefined if called outside of AppStateProvider.
  * Useful for components that may be rendered in contexts where AppStateProvider isn't available.
  */
-export function useAppStateMaybeOutsideOfProvider(selector) {
+export function useAppStateMaybeOutsideOfProvider<R>(selector: (state: AppState) => R): R | undefined {
   const $ = _c(3);
   const store = useContext(AppStoreContext);
   let t0;

+ 27 - 1
src/types/global.d.ts

@@ -58,4 +58,30 @@ declare function launchUltraplan(...args: unknown[]): void
 declare type T = any
 
 // Tungsten (internal)
-declare function TungstenPill(): JSX.Element | null
+declare function TungstenPill(props?: { key?: string; selected?: boolean }): JSX.Element | null
+
+// ============================================================================
+// Build-time constants — replaced by Bun bundler, polyfilled at runtime
+// Using `string` (not literal types) so comparisons don't produce TS2367
+declare const BUILD_TARGET: string
+declare const BUILD_ENV: string
+declare const INTERFACE_TYPE: string
+
+// ============================================================================
+// Bun text/file loaders — allow importing non-TS assets as strings
+declare module '*.md' {
+  const content: string
+  export default content
+}
+declare module '*.txt' {
+  const content: string
+  export default content
+}
+declare module '*.html' {
+  const content: string
+  export default content
+}
+declare module '*.css' {
+  const content: string
+  export default content
+}

+ 61 - 39
src/types/message.ts

@@ -1,40 +1,62 @@
 // Auto-generated stub — replace with real implementation
-export type Message = any;
-export type AssistantMessage = any;
-export type AttachmentMessage<T = any> = any;
-export type ProgressMessage<T = any> = any;
-export type SystemLocalCommandMessage = any;
-export type SystemMessage = any;
-export type UserMessage = any;
-export type NormalizedUserMessage = any;
-export type RequestStartEvent = any;
-export type StreamEvent = any;
-export type SystemCompactBoundaryMessage = any;
-export type TombstoneMessage = any;
-export type ToolUseSummaryMessage = any;
-export type MessageOrigin = any;
-export type CompactMetadata = any;
-export type SystemAPIErrorMessage = any;
-export type SystemFileSnapshotMessage = any;
-export type NormalizedAssistantMessage<T = any> = any;
-export type NormalizedMessage = any;
-export type PartialCompactDirection = any;
-export type StopHookInfo = any;
-export type SystemAgentsKilledMessage = any;
-export type SystemApiMetricsMessage = any;
-export type SystemAwaySummaryMessage = any;
-export type SystemBridgeStatusMessage = any;
-export type SystemInformationalMessage = any;
-export type SystemMemorySavedMessage = any;
-export type SystemMessageLevel = any;
-export type SystemMicrocompactBoundaryMessage = any;
-export type SystemPermissionRetryMessage = any;
-export type SystemScheduledTaskFireMessage = any;
-export type SystemStopHookSummaryMessage = any;
-export type SystemTurnDurationMessage = any;
-export type GroupedToolUseMessage = any;
-export type RenderableMessage = any;
-export type CollapsedReadSearchGroup = any;
-export type CollapsibleMessage = any;
-export type HookResultMessage = any;
-export type SystemThinkingMessage = any;
+import type { UUID } from 'crypto'
+
+/**
+ * Base message type with discriminant `type` field and common properties.
+ * Individual message subtypes (UserMessage, AssistantMessage, etc.) extend
+ * this with narrower `type` literals and additional fields.
+ */
+export type MessageType = 'user' | 'assistant' | 'system' | 'attachment' | 'progress'
+export type Message = {
+  type: MessageType
+  uuid: UUID
+  isMeta?: boolean
+  isCompactSummary?: boolean
+  toolUseResult?: unknown
+  isVisibleInTranscriptOnly?: boolean
+  message?: {
+    role?: string
+    content?: string | Array<{ type: string; text?: string; [key: string]: unknown }>
+    usage?: Record<string, unknown>
+    [key: string]: unknown
+  }
+  [key: string]: unknown
+}
+export type AssistantMessage = Message & { type: 'assistant' };
+export type AttachmentMessage<T = unknown> = Message & { type: 'attachment' };
+export type ProgressMessage<T = unknown> = Message & { type: 'progress' };
+export type SystemLocalCommandMessage = Message & { type: 'system' };
+export type SystemMessage = Message & { type: 'system' };
+export type UserMessage = Message & { type: 'user' };
+export type NormalizedUserMessage = UserMessage;
+export type RequestStartEvent = { type: string; [key: string]: unknown };
+export type StreamEvent = { type: string; [key: string]: unknown };
+export type SystemCompactBoundaryMessage = Message & { type: 'system' };
+export type TombstoneMessage = Message;
+export type ToolUseSummaryMessage = Message;
+export type MessageOrigin = string;
+export type CompactMetadata = Record<string, unknown>;
+export type SystemAPIErrorMessage = Message & { type: 'system' };
+export type SystemFileSnapshotMessage = Message & { type: 'system' };
+export type NormalizedAssistantMessage<T = unknown> = AssistantMessage;
+export type NormalizedMessage = Message;
+export type PartialCompactDirection = string;
+export type StopHookInfo = Record<string, unknown>;
+export type SystemAgentsKilledMessage = Message & { type: 'system' };
+export type SystemApiMetricsMessage = Message & { type: 'system' };
+export type SystemAwaySummaryMessage = Message & { type: 'system' };
+export type SystemBridgeStatusMessage = Message & { type: 'system' };
+export type SystemInformationalMessage = Message & { type: 'system' };
+export type SystemMemorySavedMessage = Message & { type: 'system' };
+export type SystemMessageLevel = string;
+export type SystemMicrocompactBoundaryMessage = Message & { type: 'system' };
+export type SystemPermissionRetryMessage = Message & { type: 'system' };
+export type SystemScheduledTaskFireMessage = Message & { type: 'system' };
+export type SystemStopHookSummaryMessage = Message & { type: 'system' };
+export type SystemTurnDurationMessage = Message & { type: 'system' };
+export type GroupedToolUseMessage = Message;
+export type RenderableMessage = Message;
+export type CollapsedReadSearchGroup = Message;
+export type CollapsibleMessage = Message;
+export type HookResultMessage = Message;
+export type SystemThinkingMessage = Message & { type: 'system' };

+ 9 - 2
src/types/messageQueueTypes.ts

@@ -1,3 +1,10 @@
 // Auto-generated stub — replace with real implementation
-export type QueueOperationMessage = any;
-export type QueueOperation = any;
+export type QueueOperationMessage = {
+  type: 'queue-operation'
+  operation: QueueOperation
+  timestamp: string
+  sessionId: string
+  content?: string
+  [key: string]: unknown
+}
+export type QueueOperation = 'enqueue' | 'dequeue' | 'remove' | string;

+ 2 - 1
src/types/sdk-stubs.d.ts

@@ -94,7 +94,8 @@ declare module "*/sdk/coreTypes.generated.js" {
   export type SDKUserMessage = { type: "user"; content: unknown; uuid: string; [key: string]: unknown }
   export type SDKUserMessageReplay = SDKUserMessage
   export type SDKAssistantMessage = { type: "assistant"; content: unknown; [key: string]: unknown }
-  export type SDKAssistantMessageError = { type: "assistant_error"; error: unknown; [key: string]: unknown }
+  export type SDKAssistantErrorMessage = { type: "assistant_error"; error: unknown; [key: string]: unknown }
+  export type SDKAssistantMessageError = 'authentication_failed' | 'billing_error' | 'rate_limit' | 'invalid_request' | 'server_error' | 'unknown' | 'max_output_tokens'
   export type SDKPartialAssistantMessage = { type: "partial_assistant"; [key: string]: unknown }
   export type SDKResultMessage = { type: "result"; [key: string]: unknown }
   export type SDKResultSuccess = { type: "result_success"; [key: string]: unknown }

+ 83 - 1
src/utils/hooks.ts

@@ -486,8 +486,87 @@ function parseHttpHookOutput(body: string): {
   }
 }
 
+/** Typed representation of sync hook JSON output, matching the syncHookResponseSchema Zod schema. */
+interface TypedSyncHookOutput {
+  continue?: boolean
+  suppressOutput?: boolean
+  stopReason?: string
+  decision?: 'approve' | 'block'
+  reason?: string
+  systemMessage?: string
+  hookSpecificOutput?:
+    | {
+        hookEventName: 'PreToolUse'
+        permissionDecision?: 'ask' | 'deny' | 'allow' | 'passthrough'
+        permissionDecisionReason?: string
+        updatedInput?: Record<string, unknown>
+        additionalContext?: string
+      }
+    | {
+        hookEventName: 'UserPromptSubmit'
+        additionalContext?: string
+      }
+    | {
+        hookEventName: 'SessionStart'
+        additionalContext?: string
+        initialUserMessage?: string
+        watchPaths?: string[]
+      }
+    | {
+        hookEventName: 'Setup'
+        additionalContext?: string
+      }
+    | {
+        hookEventName: 'SubagentStart'
+        additionalContext?: string
+      }
+    | {
+        hookEventName: 'PostToolUse'
+        additionalContext?: string
+        updatedMCPToolOutput?: unknown
+      }
+    | {
+        hookEventName: 'PostToolUseFailure'
+        additionalContext?: string
+      }
+    | {
+        hookEventName: 'PermissionDenied'
+        retry?: boolean
+      }
+    | {
+        hookEventName: 'Notification'
+        additionalContext?: string
+      }
+    | {
+        hookEventName: 'PermissionRequest'
+        decision?: PermissionRequestResult
+      }
+    | {
+        hookEventName: 'Elicitation'
+        action?: 'accept' | 'decline' | 'cancel'
+        content?: Record<string, unknown>
+      }
+    | {
+        hookEventName: 'ElicitationResult'
+        action?: 'accept' | 'decline' | 'cancel'
+        content?: Record<string, unknown>
+      }
+    | {
+        hookEventName: 'CwdChanged'
+        watchPaths?: string[]
+      }
+    | {
+        hookEventName: 'FileChanged'
+        watchPaths?: string[]
+      }
+    | {
+        hookEventName: 'WorktreeCreate'
+        worktreePath: string
+      }
+}
+
 function processHookJSONOutput({
-  json,
+  json: rawJson,
   command,
   hookName,
   toolUseID,
@@ -511,6 +590,9 @@ function processHookJSONOutput({
 }): Partial<HookResult> {
   const result: Partial<HookResult> = {}
 
+  // Cast to typed interface for type-safe property access
+  const json = rawJson as TypedSyncHookOutput
+
   // At this point we know it's a sync response
   const syncJson = json
 

+ 2 - 2
src/utils/sessionStorage.ts

@@ -1033,7 +1033,7 @@ class Project {
           'sourceToolAssistantUUID' in message &&
           message.sourceToolAssistantUUID
         ) {
-          effectiveParentUuid = message.sourceToolAssistantUUID
+          effectiveParentUuid = message.sourceToolAssistantUUID as UUID
         }
 
         const transcriptMessage: TranscriptMessage = {
@@ -2120,7 +2120,7 @@ function recoverOrphanedParallelToolResults(
   chain: TranscriptMessage[],
   seen: Set<UUID>,
 ): TranscriptMessage[] {
-  type ChainAssistant = Extract<TranscriptMessage, { type: 'assistant' }>
+  type ChainAssistant = TranscriptMessage & { type: 'assistant' }
   const chainAssistants = chain.filter(
     (m): m is ChainAssistant => m.type === 'assistant',
   )