mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
更新大量 tsx 原始文件; 已经迁移 login panel; 部分 (#121)
* style(B1-1): 格式化 ink/buddy/cli/context/screens/tasks/services/keybindings/state (43 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 修复了 Box.tsx 和 ScrollBox.tsx 中无效的 global.d.ts import。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-2): 格式化 commands (79 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-3): 格式化 components/messages,permissions,mcp,sandbox,shell (104 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-5): 格式化 components其余 + hooks + tools (232 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-6): 格式化 main/entrypoints/utils/moreright (21 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: 更新 README,新增 Run.ps1/TODO.md,删除 V6.md - README.md: 大幅重写,更详细版本历史和配置示例 - Run.ps1: 新增 Windows 启动脚本 - TODO.md: 新增包完成清单 - V6.md: 删除(架构重构规划已不适用) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 修复以前的问题 * fix: 修复 login 面板的问题 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
75
README.md
75
README.md
@@ -16,28 +16,48 @@
|
||||
|
||||
[Discord 群组](https://discord.gg/qZU6zS7Q)
|
||||
|
||||
- [x] V4 — 测试补全、[Buddy](https://ccb.agent-aura.top/docs/features/buddy)、[Auto Mode](https://ccb.agent-aura.top/docs/safety/auto-mode)、环境变量 Feature 开关
|
||||
- [x] V5 — [Sentry](https://ccb.agent-aura.top/docs/internals/sentry-setup) / [GrowthBook](https://ccb.agent-aura.top/docs/internals/growthbook-adapter) 企业监控、[自定义 Login](https://ccb.agent-aura.top/docs/features/custom-platform-login)、[OpenAI 兼容](https://ccb.agent-aura.top/docs/plans/openai-compatibility)、[Web Search](https://ccb.agent-aura.top/docs/features/web-browser-tool)、[Computer Use](https://ccb.agent-aura.top/docs/features/computer-use) / [Chrome Use](https://ccb.agent-aura.top/docs/features/claude-in-chrome-mcp)、[Voice Mode](https://ccb.agent-aura.top/docs/features/voice-mode)、[Bridge Mode](https://ccb.agent-aura.top/docs/features/bridge-mode)、[/dream 记忆整理](https://ccb.agent-aura.top/docs/features/auto-dream)
|
||||
- [ ] V6 — 大规模重构石山代码,全面模块分包(全新分支,main 封存为历史版本)
|
||||
赞助商占位符
|
||||
|
||||
- [x] v1 会完成跑通及基本的类型检查通过;
|
||||
- [x] V2 会完整实现工程化配套设施;
|
||||
- [ ] Biome 格式化可能不会先实施, 避免代码冲突
|
||||
- [x] 构建流水线完成, 产物 Node/Bun 都可以运行
|
||||
- [x] V3 会写大量文档, 完善文档站点
|
||||
- [x] V4 会完成大量的测试文件, 以提高稳定性
|
||||
- [x] Buddy 小宠物回来啦 [文档](https://ccb.agent-aura.top/docs/features/buddy)
|
||||
- [x] Auto Mode 回归 [文档](https://ccb.agent-aura.top/docs/safety/auto-mode)
|
||||
- [x] 所有 Feature 现在可以通过环境变量配置, 而不是垃圾的 bun --feature
|
||||
- [x] V5 支持企业级的监控上报功能, 补全缺失的工具, 解除限制
|
||||
- [x] 移除牢 A 的反蒸馏代码!!!
|
||||
- [x] 补全 web search 能力(用的 Bing 搜索)!!! [文档](https://ccb.agent-aura.top/docs/features/web-browser-tool)
|
||||
- [x] 支持 Debug [文档](https://ccb.agent-aura.top/docs/features/debug-mode)
|
||||
- [x] 关闭自动更新;
|
||||
- [x] 添加自定义 sentry 错误上报支持 [文档](https://ccb.agent-aura.top/docs/internals/sentry-setup)
|
||||
- [x] 添加自定义 GrowthBook 支持 (GB 也是开源的, 现在你可以配置一个自定义的遥控平台) [文档](https://ccb.agent-aura.top/docs/internals/growthbook-adapter)
|
||||
- [x] 自定义 login 模式, 大家可以用这个配置 Claude 的模型! [文档](https://ccb.agent-aura.top/docs/features/custom-platform-login)
|
||||
- [x] Remote Control / Bridge Mode 支持 [文档](https://ccb.agent-aura.top/docs/features/bridge-mode)
|
||||
- [x] 修复搜索工具的 rg 缺失问题(需要重新 bun i)
|
||||
- [x] OpenAI 接口兼容! /login 然后配置 OpenAI 平台即可! [文档](https://ccb.agent-aura.top/docs/plans/openai-compatibility)
|
||||
- [x] Any Use
|
||||
- [x] 由于 Chrome Use 和 Computer Use 原本都是未完全验证的能力, 还是比较建议大家用社区里面的 MCP 支持
|
||||
- [x] Chrome use 支持 (浏览器插件要订阅权限 ) 感谢 @amDosion [文档](https://ccb.agent-aura.top/docs/features/claude-in-chrome-mcp)
|
||||
- [x] 普通用户可以使用 [chrome-devtools-mcp](https://github.com/ChromeDevTools/chrome-devtools-mcp/) 替代, 比较不那么折腾
|
||||
- [x] Computer use 支持 感谢 @amDosion [文档](https://ccb.agent-aura.top/docs/features/computer-use)
|
||||
- [x] Mac 上可以用这个项目 [computer-use-mcp](https://github.com/domdomegg/computer-use-mcp)
|
||||
- 注意这个库的命名方式与官方冲突了, 需要改为 `claude mcp add --scope user --transport stdio computer-use-mcp -- npx -y computer-use-mcp`
|
||||
- [x] /voice 支持 @amDosion [文档](https://ccb.agent-aura.top/docs/features/voice-mode)
|
||||
- [x] /dream 记忆整理命令(手动 + 自动后台触发) [文档](https://ccb.agent-aura.top/docs/features/auto-dream)
|
||||
- [ ] V6 大规模重构石山代码, 全面模块分包
|
||||
- [ ] V6 将会为全新分支, 届时 main 分支将会封存为历史版本
|
||||
|
||||
## 快速开始(安装版)
|
||||
> 我不知道这个项目还会存在多久, Star + Fork + git clone + .zip 包最稳健; 说白了就是扛旗项目, 看看能走多远
|
||||
>
|
||||
> 这个项目更新很快, 后台有 Opus 持续优化, 几乎几个小时就有新变化;
|
||||
>
|
||||
> Claude 已经烧了 1000$ 以上, 没钱了, 换成 GLM 继续玩; @zai-org GLM 5.1 非常可以;
|
||||
>
|
||||
|
||||
不用克隆仓库, 从 NPM 下载后, 直接使用
|
||||
|
||||
```sh
|
||||
bun i -g claude-code-best
|
||||
bun pm -g trust claude-code-best
|
||||
ccb # 直接打开 claude code
|
||||
```
|
||||
|
||||
国内对 github 网络较差的, 需要先设置这个环境变量
|
||||
|
||||
```bash
|
||||
DEFAULT_RELEASE_BASE=https://ghproxy.net/https://github.com/microsoft/ripgrep-prebuilt/releases/download/v15.0.1
|
||||
```
|
||||
|
||||
## 快速开始(源码版)
|
||||
## 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
@@ -76,7 +96,7 @@ bun run build
|
||||
|
||||
### 新人配置 /login
|
||||
|
||||
首次运行后,在 REPL 中输入 `/login` 命令进入登录配置界面,选择 **Custom Platform** 即可对接第三方 API 兼容服务(无需 Anthropic 官方账号)。
|
||||
首次运行后,在 REPL 中输入 `/login` 命令进入登录配置界面,选择 **Anthropic Compatible** 即可对接第三方 API 兼容服务(无需 Anthropic 官方账号)。
|
||||
|
||||
需要填写的字段:
|
||||
|
||||
@@ -89,7 +109,22 @@ bun run build
|
||||
| Opus Model | 高性能模型 ID | `claude-opus-4-6` |
|
||||
|
||||
- **Tab / Shift+Tab** 切换字段,**Enter** 确认并跳到下一个,最后一个字段按 Enter 保存
|
||||
- 模型字段会自动读取当前环境变量预填
|
||||
- 配置保存到 `~/.claude/settings.json` 的 `env` 字段,保存后立即生效
|
||||
|
||||
也可以直接编辑 `~/.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.example.com/v1",
|
||||
"ANTHROPIC_AUTH_TOKEN": "sk-xxx",
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5-20251001",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-6",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-6"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 支持所有 Anthropic API 兼容服务(如 OpenRouter、AWS Bedrock 代理等),只要接口兼容 Messages API 即可。
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ If you encounter a bug, please open an issue — we'll prioritize it.
|
||||
|
||||
### First-time Setup /login
|
||||
|
||||
After the first run, enter `/login` in the REPL to access the login configuration screen. Select **Custom Platform** to connect to third-party API-compatible services (no Anthropic account required).
|
||||
After the first run, enter `/login` in the REPL to access the login configuration screen. Select **Anthropic Compatible** to connect to third-party API-compatible services (no Anthropic account required).
|
||||
|
||||
Fields to fill in:
|
||||
|
||||
|
||||
2
Run.ps1
Normal file
2
Run.ps1
Normal file
@@ -0,0 +1,2 @@
|
||||
bun install
|
||||
bun run dev --dangerously-skip-permissions
|
||||
26
TODO.md
Normal file
26
TODO.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# TODO
|
||||
|
||||
尽可能实现下面的包, 使得与主包的关系完全吻合
|
||||
|
||||
## Packages
|
||||
|
||||
- [x] `url-handler-napi` — URL 处理 NAPI 模块 (签名修正,保持 null fallback)
|
||||
- [x] `modifiers-napi` — 修饰键检测 NAPI 模块 (Bun FFI + Carbon)
|
||||
- [x] `audio-capture-napi` — 音频捕获 NAPI 模块 (SoX/arecord)
|
||||
- [x] `color-diff-napi` — 颜色差异计算 NAPI 模块 (纯 TS 实现)
|
||||
- [x] `image-processor-napi` — 图像处理 NAPI 模块 (sharp + osascript 剪贴板)
|
||||
|
||||
- [x] `@ant/computer-use-swift` — Computer Use Swift 原生模块 (macOS JXA/screencapture 实现)
|
||||
- [x] `@ant/computer-use-mcp` — Computer Use MCP 服务 (类型安全 stub + sentinel apps + targetImageSize)
|
||||
- [x] `@ant/computer-use-input` — Computer Use 输入模块 (macOS AppleScript/JXA 实现)
|
||||
<!-- - [ ] `@ant/claude-for-chrome-mcp` — Chrome MCP 扩展 -->
|
||||
|
||||
## 工程化能力
|
||||
|
||||
- [x] 代码格式化与校验
|
||||
- [x] 冗余代码检查
|
||||
- [x] git hook 的配置
|
||||
- [x] 代码健康度检查
|
||||
- [x] Biome lint 规则调优(适配反编译代码,关闭格式化避免大规模 diff)
|
||||
- [x] 单元测试基础设施搭建 (test runner 配置)
|
||||
- [x] CI/CD 流水线 (GitHub Actions)
|
||||
49
docs/pr-120-merge-plan.md
Normal file
49
docs/pr-120-merge-plan.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# PR #120 分批合入方案
|
||||
|
||||
> 分支: `pr-sobird-120` → `main`
|
||||
> 总计: 558 files / +102,830 / -101,373
|
||||
|
||||
## 执行进度
|
||||
|
||||
### B1: 552 文件格式化 — 拆分为 6 个子批次
|
||||
|
||||
#### B1-1: ink + buddy + cli + context + screens + tasks + services + keybindings (43 files) ✓
|
||||
- [x] 合入: src/ink/ (17), src/buddy/ (2), src/cli/ (2), src/context/ (9), src/screens/ (3), src/tasks/ (4), src/services/ (3), src/keybindings/ (2), src/state/ (1)
|
||||
- [x] 验证 `bun run build` 通过 ✓ (475 files)
|
||||
|
||||
#### B1-2: commands (79 files) ✓
|
||||
- [x] 合入: src/commands/ (79 files)
|
||||
- [x] 验证 `bun run build` 通过 ✓
|
||||
|
||||
#### B1-3: components/messages + permissions + mcp + sandbox + shell (104 files) ✓
|
||||
- [x] 合入: src/components/messages/ (39), src/components/permissions/ (39), src/components/mcp/ (11), src/components/sandbox/ (5), src/components/shell/ (4)
|
||||
- [x] 验证 `bun run build` 通过 ✓
|
||||
|
||||
#### B1-4: components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files) ✓
|
||||
- [x] 合入: src/components/PromptInput/ (13), src/components/FeedbackSurvey/ (6), src/components/tasks/ (12), src/components/agents/ (17), src/components/skills/ (1), src/components/design-system/ (14), src/components/wizard/ (3)
|
||||
- [x] 验证 `bun run build` 通过 ✓
|
||||
|
||||
#### B1-5: components 其余 + hooks + tools (232 files) ✓
|
||||
- [x] 合入: src/components/ 其余目录 (~169), src/hooks/ (28), src/tools/ (35)
|
||||
- [x] 验证 `bun run build` 通过 ✓
|
||||
|
||||
#### B1-6: 根目录 + utils + 其他零散文件 (21 files) ✓
|
||||
- [x] 合入: src/main.tsx, src/dialogLaunchers.tsx, src/replLauncher.tsx, src/interactiveHelpers.tsx, src/entrypoints/, src/moreright/, src/utils/ (15)
|
||||
- [x] 验证 `bun run build` 通过 ✓
|
||||
|
||||
### B2: 45 文件 USER_TYPE 替换 (commit `fc200fd`)
|
||||
- [ ] cherry-pick USER_TYPE 提交
|
||||
- [ ] 检查替换是否完整无遗漏
|
||||
- [ ] 验证 `bun run build` 通过
|
||||
|
||||
### B3: 文档变更 (README / Run.ps1 / TODO.md / V6.md)
|
||||
- [ ] 合入 README.md, Run.ps1, TODO.md
|
||||
- [ ] 删除 V6.md
|
||||
- [ ] 验证无破坏
|
||||
|
||||
### B4: 构建配置变更 — 跳过
|
||||
|
||||
### 最终验证
|
||||
- [ ] `bun run build` 完整构建
|
||||
- [ ] `bun test` 测试通过
|
||||
- [ ] git log 确认提交历史清晰
|
||||
@@ -1,162 +1,114 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import figures from 'figures';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
import { stringWidth } from '../ink/stringWidth.js';
|
||||
import { Box, Text } from '../ink.js';
|
||||
import { useAppState, useSetAppState } from '../state/AppState.js';
|
||||
import type { AppState } from '../state/AppStateStore.js';
|
||||
import { getGlobalConfig } from '../utils/config.js';
|
||||
import { isFullscreenActive } from '../utils/fullscreen.js';
|
||||
import type { Theme } from '../utils/theme.js';
|
||||
import { getCompanion } from './companion.js';
|
||||
import { renderFace, renderSprite, spriteFrameCount } from './sprites.js';
|
||||
import { RARITY_COLORS } from './types.js';
|
||||
const TICK_MS = 500;
|
||||
const BUBBLE_SHOW = 20; // ticks → ~10s at 500ms
|
||||
const FADE_WINDOW = 6; // last ~3s the bubble dims so you know it's about to go
|
||||
const PET_BURST_MS = 2500; // how long hearts float after /buddy pet
|
||||
import { feature } from 'bun:bundle'
|
||||
import figures from 'figures'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js'
|
||||
import { stringWidth } from '../ink/stringWidth.js'
|
||||
import { Box, Text } from '../ink.js'
|
||||
import { useAppState, useSetAppState } from '../state/AppState.js'
|
||||
import type { AppState } from '../state/AppStateStore.js'
|
||||
import { getGlobalConfig } from '../utils/config.js'
|
||||
import { isFullscreenActive } from '../utils/fullscreen.js'
|
||||
import type { Theme } from '../utils/theme.js'
|
||||
import { getCompanion } from './companion.js'
|
||||
import { renderFace, renderSprite, spriteFrameCount } from './sprites.js'
|
||||
import { RARITY_COLORS } from './types.js'
|
||||
|
||||
const TICK_MS = 500
|
||||
const BUBBLE_SHOW = 20 // ticks → ~10s at 500ms
|
||||
const FADE_WINDOW = 6 // last ~3s the bubble dims so you know it's about to go
|
||||
const PET_BURST_MS = 2500 // how long hearts float after /buddy pet
|
||||
|
||||
// Idle sequence: mostly rest (frame 0), occasional fidget (frames 1-2), rare blink.
|
||||
// Sequence indices map to sprite frames; -1 means "blink on frame 0".
|
||||
const IDLE_SEQUENCE = [0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 2, 0, 0, 0];
|
||||
const IDLE_SEQUENCE = [0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 2, 0, 0, 0]
|
||||
|
||||
// Hearts float up-and-out over 5 ticks (~2.5s). Prepended above the sprite.
|
||||
const H = figures.heart;
|
||||
const PET_HEARTS = [` ${H} ${H} `, ` ${H} ${H} ${H} `, ` ${H} ${H} ${H} `, `${H} ${H} ${H} `, '· · · '];
|
||||
const H = figures.heart
|
||||
const PET_HEARTS = [
|
||||
` ${H} ${H} `,
|
||||
` ${H} ${H} ${H} `,
|
||||
` ${H} ${H} ${H} `,
|
||||
`${H} ${H} ${H} `,
|
||||
'· · · ',
|
||||
]
|
||||
|
||||
function wrap(text: string, width: number): string[] {
|
||||
const words = text.split(' ');
|
||||
const lines: string[] = [];
|
||||
let cur = '';
|
||||
const words = text.split(' ')
|
||||
const lines: string[] = []
|
||||
let cur = ''
|
||||
for (const w of words) {
|
||||
if (cur.length + w.length + 1 > width && cur) {
|
||||
lines.push(cur);
|
||||
cur = w;
|
||||
lines.push(cur)
|
||||
cur = w
|
||||
} else {
|
||||
cur = cur ? `${cur} ${w}` : w;
|
||||
cur = cur ? `${cur} ${w}` : w
|
||||
}
|
||||
}
|
||||
if (cur) lines.push(cur);
|
||||
return lines;
|
||||
if (cur) lines.push(cur)
|
||||
return lines
|
||||
}
|
||||
function SpeechBubble(t0) {
|
||||
const $ = _c(31);
|
||||
const {
|
||||
text,
|
||||
color,
|
||||
fading,
|
||||
tail
|
||||
} = t0;
|
||||
let T0;
|
||||
let borderColor;
|
||||
let t1;
|
||||
let t2;
|
||||
let t3;
|
||||
let t4;
|
||||
let t5;
|
||||
let t6;
|
||||
if ($[0] !== color || $[1] !== fading || $[2] !== text) {
|
||||
const lines = wrap(text, 30);
|
||||
borderColor = fading ? "inactive" : color;
|
||||
T0 = Box;
|
||||
t1 = "column";
|
||||
t2 = "round";
|
||||
t3 = borderColor;
|
||||
t4 = 1;
|
||||
t5 = 34;
|
||||
let t7;
|
||||
if ($[11] !== fading) {
|
||||
t7 = (l, i) => <Text key={i} italic={true} dimColor={!fading} color={fading ? "inactive" : undefined}>{l}</Text>;
|
||||
$[11] = fading;
|
||||
$[12] = t7;
|
||||
} else {
|
||||
t7 = $[12];
|
||||
}
|
||||
t6 = lines.map(t7);
|
||||
$[0] = color;
|
||||
$[1] = fading;
|
||||
$[2] = text;
|
||||
$[3] = T0;
|
||||
$[4] = borderColor;
|
||||
$[5] = t1;
|
||||
$[6] = t2;
|
||||
$[7] = t3;
|
||||
$[8] = t4;
|
||||
$[9] = t5;
|
||||
$[10] = t6;
|
||||
} else {
|
||||
T0 = $[3];
|
||||
borderColor = $[4];
|
||||
t1 = $[5];
|
||||
t2 = $[6];
|
||||
t3 = $[7];
|
||||
t4 = $[8];
|
||||
t5 = $[9];
|
||||
t6 = $[10];
|
||||
|
||||
function SpeechBubble({
|
||||
text,
|
||||
color,
|
||||
fading,
|
||||
tail,
|
||||
}: {
|
||||
text: string
|
||||
color: keyof Theme
|
||||
fading: boolean
|
||||
tail: 'down' | 'right'
|
||||
}): React.ReactNode {
|
||||
const lines = wrap(text, 30)
|
||||
const borderColor = fading ? 'inactive' : color
|
||||
const bubble = (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={borderColor}
|
||||
paddingX={1}
|
||||
width={34}
|
||||
>
|
||||
{lines.map((l, i) => (
|
||||
<Text
|
||||
key={i}
|
||||
italic
|
||||
dimColor={!fading}
|
||||
color={fading ? 'inactive' : undefined}
|
||||
>
|
||||
{l}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
if (tail === 'right') {
|
||||
return (
|
||||
<Box flexDirection="row" alignItems="center">
|
||||
{bubble}
|
||||
<Text color={borderColor}>─</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
let t7;
|
||||
if ($[13] !== T0 || $[14] !== t1 || $[15] !== t2 || $[16] !== t3 || $[17] !== t4 || $[18] !== t5 || $[19] !== t6) {
|
||||
t7 = <T0 flexDirection={t1} borderStyle={t2} borderColor={t3} paddingX={t4} width={t5}>{t6}</T0>;
|
||||
$[13] = T0;
|
||||
$[14] = t1;
|
||||
$[15] = t2;
|
||||
$[16] = t3;
|
||||
$[17] = t4;
|
||||
$[18] = t5;
|
||||
$[19] = t6;
|
||||
$[20] = t7;
|
||||
} else {
|
||||
t7 = $[20];
|
||||
}
|
||||
const bubble = t7;
|
||||
if (tail === "right") {
|
||||
let t8;
|
||||
if ($[21] !== borderColor) {
|
||||
t8 = <Text color={borderColor}>─</Text>;
|
||||
$[21] = borderColor;
|
||||
$[22] = t8;
|
||||
} else {
|
||||
t8 = $[22];
|
||||
}
|
||||
let t9;
|
||||
if ($[23] !== bubble || $[24] !== t8) {
|
||||
t9 = <Box flexDirection="row" alignItems="center">{bubble}{t8}</Box>;
|
||||
$[23] = bubble;
|
||||
$[24] = t8;
|
||||
$[25] = t9;
|
||||
} else {
|
||||
t9 = $[25];
|
||||
}
|
||||
return t9;
|
||||
}
|
||||
let t8;
|
||||
if ($[26] !== borderColor) {
|
||||
t8 = <Box flexDirection="column" alignItems="flex-end" paddingRight={6}><Text color={borderColor}>╲ </Text><Text color={borderColor}>╲</Text></Box>;
|
||||
$[26] = borderColor;
|
||||
$[27] = t8;
|
||||
} else {
|
||||
t8 = $[27];
|
||||
}
|
||||
let t9;
|
||||
if ($[28] !== bubble || $[29] !== t8) {
|
||||
t9 = <Box flexDirection="column" alignItems="flex-end" marginRight={1}>{bubble}{t8}</Box>;
|
||||
$[28] = bubble;
|
||||
$[29] = t8;
|
||||
$[30] = t9;
|
||||
} else {
|
||||
t9 = $[30];
|
||||
}
|
||||
return t9;
|
||||
return (
|
||||
<Box flexDirection="column" alignItems="flex-end" marginRight={1}>
|
||||
{bubble}
|
||||
<Box flexDirection="column" alignItems="flex-end" paddingRight={6}>
|
||||
<Text color={borderColor}>╲ </Text>
|
||||
<Text color={borderColor}>╲</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
export const MIN_COLS_FOR_FULL_SPRITE = 100;
|
||||
const SPRITE_BODY_WIDTH = 12;
|
||||
const NAME_ROW_PAD = 2; // focused state wraps name in spaces: ` name `
|
||||
const SPRITE_PADDING_X = 2;
|
||||
const BUBBLE_WIDTH = 36; // SpeechBubble box (34) + tail column
|
||||
const NARROW_QUIP_CAP = 24;
|
||||
|
||||
export const MIN_COLS_FOR_FULL_SPRITE = 100
|
||||
const SPRITE_BODY_WIDTH = 12
|
||||
const NAME_ROW_PAD = 2 // focused state wraps name in spaces: ` name `
|
||||
const SPRITE_PADDING_X = 2
|
||||
const BUBBLE_WIDTH = 36 // SpeechBubble box (34) + tail column
|
||||
const NARROW_QUIP_CAP = 24
|
||||
|
||||
function spriteColWidth(nameWidth: number): number {
|
||||
return Math.max(SPRITE_BODY_WIDTH, nameWidth + NAME_ROW_PAD);
|
||||
return Math.max(SPRITE_BODY_WIDTH, nameWidth + NAME_ROW_PAD)
|
||||
}
|
||||
|
||||
// Width the sprite area consumes. PromptInput subtracts this so text wraps
|
||||
@@ -164,115 +116,171 @@ function spriteColWidth(nameWidth: number): number {
|
||||
// width); in non-fullscreen it sits inline and needs BUBBLE_WIDTH more.
|
||||
// Narrow terminals: 0 — REPL.tsx stacks the one-liner on its own row
|
||||
// (above input in fullscreen, below in scrollback), so no reservation.
|
||||
export function companionReservedColumns(terminalColumns: number, speaking: boolean): number {
|
||||
if (!feature('BUDDY')) return 0;
|
||||
const companion = getCompanion();
|
||||
if (!companion || getGlobalConfig().companionMuted) return 0;
|
||||
if (terminalColumns < MIN_COLS_FOR_FULL_SPRITE) return 0;
|
||||
const nameWidth = stringWidth(companion.name);
|
||||
const bubble = speaking && !isFullscreenActive() ? BUBBLE_WIDTH : 0;
|
||||
return spriteColWidth(nameWidth) + SPRITE_PADDING_X + bubble;
|
||||
export function companionReservedColumns(
|
||||
terminalColumns: number,
|
||||
speaking: boolean,
|
||||
): number {
|
||||
if (!feature('BUDDY')) return 0
|
||||
const companion = getCompanion()
|
||||
if (!companion || getGlobalConfig().companionMuted) return 0
|
||||
if (terminalColumns < MIN_COLS_FOR_FULL_SPRITE) return 0
|
||||
const nameWidth = stringWidth(companion.name)
|
||||
const bubble = speaking && !isFullscreenActive() ? BUBBLE_WIDTH : 0
|
||||
return spriteColWidth(nameWidth) + SPRITE_PADDING_X + bubble
|
||||
}
|
||||
|
||||
export function CompanionSprite(): React.ReactNode {
|
||||
const reaction = useAppState(s => s.companionReaction);
|
||||
const petAt = useAppState(s => s.companionPetAt);
|
||||
const focused = useAppState(s => s.footerSelection === 'companion');
|
||||
const setAppState = useSetAppState();
|
||||
const {
|
||||
columns
|
||||
} = useTerminalSize();
|
||||
const [tick, setTick] = useState(0);
|
||||
const lastSpokeTick = useRef(0);
|
||||
const reaction = useAppState(s => s.companionReaction)
|
||||
const petAt = useAppState(s => s.companionPetAt)
|
||||
const focused = useAppState(s => s.footerSelection === 'companion')
|
||||
const setAppState = useSetAppState()
|
||||
const { columns } = useTerminalSize()
|
||||
const [tick, setTick] = useState(0)
|
||||
const lastSpokeTick = useRef(0)
|
||||
// Sync-during-render (not useEffect) so the first post-pet render already
|
||||
// has petStartTick=tick and petAge=0 — otherwise frame 0 is skipped.
|
||||
const [{
|
||||
petStartTick,
|
||||
forPetAt
|
||||
}, setPetStart] = useState({
|
||||
const [{ petStartTick, forPetAt }, setPetStart] = useState({
|
||||
petStartTick: 0,
|
||||
forPetAt: petAt
|
||||
});
|
||||
forPetAt: petAt,
|
||||
})
|
||||
if (petAt !== forPetAt) {
|
||||
setPetStart({
|
||||
petStartTick: tick,
|
||||
forPetAt: petAt
|
||||
});
|
||||
setPetStart({ petStartTick: tick, forPetAt: petAt })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(setT => setT((t: number) => t + 1), TICK_MS, setTick);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
const timer = setInterval(
|
||||
setT => setT((t: number) => t + 1),
|
||||
TICK_MS,
|
||||
setTick,
|
||||
)
|
||||
return () => clearInterval(timer)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!reaction) return;
|
||||
lastSpokeTick.current = tick;
|
||||
const timer = setTimeout(setA => setA((prev: AppState) => prev.companionReaction === undefined ? prev : {
|
||||
...prev,
|
||||
companionReaction: undefined
|
||||
}), BUBBLE_SHOW * TICK_MS, setAppState);
|
||||
return () => clearTimeout(timer);
|
||||
if (!reaction) return
|
||||
lastSpokeTick.current = tick
|
||||
const timer = setTimeout(
|
||||
setA =>
|
||||
setA((prev: AppState) =>
|
||||
prev.companionReaction === undefined
|
||||
? prev
|
||||
: { ...prev, companionReaction: undefined },
|
||||
),
|
||||
BUBBLE_SHOW * TICK_MS,
|
||||
setAppState,
|
||||
)
|
||||
return () => clearTimeout(timer)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- tick intentionally captured at reaction-change, not tracked
|
||||
}, [reaction, setAppState]);
|
||||
if (!feature('BUDDY')) return null;
|
||||
const companion = getCompanion();
|
||||
if (!companion || getGlobalConfig().companionMuted) return null;
|
||||
const color = RARITY_COLORS[companion.rarity];
|
||||
const colWidth = spriteColWidth(stringWidth(companion.name));
|
||||
const bubbleAge = reaction ? tick - lastSpokeTick.current : 0;
|
||||
const fading = reaction !== undefined && bubbleAge >= BUBBLE_SHOW - FADE_WINDOW;
|
||||
const petAge = petAt ? tick - petStartTick : Infinity;
|
||||
const petting = petAge * TICK_MS < PET_BURST_MS;
|
||||
}, [reaction, setAppState])
|
||||
|
||||
if (!feature('BUDDY')) return null
|
||||
const companion = getCompanion()
|
||||
if (!companion || getGlobalConfig().companionMuted) return null
|
||||
|
||||
const color = RARITY_COLORS[companion.rarity]
|
||||
const colWidth = spriteColWidth(stringWidth(companion.name))
|
||||
|
||||
const bubbleAge = reaction ? tick - lastSpokeTick.current : 0
|
||||
const fading =
|
||||
reaction !== undefined && bubbleAge >= BUBBLE_SHOW - FADE_WINDOW
|
||||
|
||||
const petAge = petAt ? tick - petStartTick : Infinity
|
||||
const petting = petAge * TICK_MS < PET_BURST_MS
|
||||
|
||||
// Narrow terminals: collapse to one-line face. When speaking, the quip
|
||||
// replaces the name beside the face (no room for a bubble).
|
||||
if (columns < MIN_COLS_FOR_FULL_SPRITE) {
|
||||
const quip = reaction && reaction.length > NARROW_QUIP_CAP ? reaction.slice(0, NARROW_QUIP_CAP - 1) + '…' : reaction;
|
||||
const label = quip ? `"${quip}"` : focused ? ` ${companion.name} ` : companion.name;
|
||||
return <Box paddingX={1} alignSelf="flex-end">
|
||||
const quip =
|
||||
reaction && reaction.length > NARROW_QUIP_CAP
|
||||
? reaction.slice(0, NARROW_QUIP_CAP - 1) + '…'
|
||||
: reaction
|
||||
const label = quip
|
||||
? `"${quip}"`
|
||||
: focused
|
||||
? ` ${companion.name} `
|
||||
: companion.name
|
||||
return (
|
||||
<Box paddingX={1} alignSelf="flex-end">
|
||||
<Text>
|
||||
{petting && <Text color="autoAccept">{figures.heart} </Text>}
|
||||
<Text bold color={color}>
|
||||
{renderFace(companion)}
|
||||
</Text>{' '}
|
||||
<Text italic dimColor={!focused && !reaction} bold={focused} inverse={focused && !reaction} color={reaction ? fading ? 'inactive' : color : focused ? color : undefined}>
|
||||
<Text
|
||||
italic
|
||||
dimColor={!focused && !reaction}
|
||||
bold={focused}
|
||||
inverse={focused && !reaction}
|
||||
color={
|
||||
reaction
|
||||
? fading
|
||||
? 'inactive'
|
||||
: color
|
||||
: focused
|
||||
? color
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
const frameCount = spriteFrameCount(companion.species);
|
||||
const heartFrame = petting ? PET_HEARTS[petAge % PET_HEARTS.length] : null;
|
||||
let spriteFrame: number;
|
||||
let blink = false;
|
||||
const frameCount = spriteFrameCount(companion.species)
|
||||
const heartFrame = petting ? PET_HEARTS[petAge % PET_HEARTS.length] : null
|
||||
|
||||
let spriteFrame: number
|
||||
let blink = false
|
||||
if (reaction || petting) {
|
||||
// Excited: cycle all fidget frames fast
|
||||
spriteFrame = tick % frameCount;
|
||||
spriteFrame = tick % frameCount
|
||||
} else {
|
||||
const step = IDLE_SEQUENCE[tick % IDLE_SEQUENCE.length]!;
|
||||
const step = IDLE_SEQUENCE[tick % IDLE_SEQUENCE.length]!
|
||||
if (step === -1) {
|
||||
spriteFrame = 0;
|
||||
blink = true;
|
||||
spriteFrame = 0
|
||||
blink = true
|
||||
} else {
|
||||
spriteFrame = step % frameCount;
|
||||
spriteFrame = step % frameCount
|
||||
}
|
||||
}
|
||||
const body = renderSprite(companion, spriteFrame).map(line => blink ? line.replaceAll(companion.eye, '-') : line);
|
||||
const sprite = heartFrame ? [heartFrame, ...body] : body;
|
||||
|
||||
const body = renderSprite(companion, spriteFrame).map(line =>
|
||||
blink ? line.replaceAll(companion.eye, '-') : line,
|
||||
)
|
||||
const sprite = heartFrame ? [heartFrame, ...body] : body
|
||||
|
||||
// Name row doubles as hint row — unfocused shows dim name + ↓ discovery,
|
||||
// focused shows inverse name. The enter-to-open hint lives in
|
||||
// PromptInputFooter's right column so this row stays one line and the
|
||||
// sprite doesn't jump up when selected. flexShrink=0 stops the
|
||||
// inline-bubble row wrapper from squeezing the sprite to fit.
|
||||
const spriteColumn = <Box flexDirection="column" flexShrink={0} alignItems="center" width={colWidth}>
|
||||
{sprite.map((line, i) => <Text key={i} color={i === 0 && heartFrame ? 'autoAccept' : color}>
|
||||
const spriteColumn = (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
flexShrink={0}
|
||||
alignItems="center"
|
||||
width={colWidth}
|
||||
>
|
||||
{sprite.map((line, i) => (
|
||||
<Text key={i} color={i === 0 && heartFrame ? 'autoAccept' : color}>
|
||||
{line}
|
||||
</Text>)}
|
||||
<Text italic bold={focused} dimColor={!focused} color={focused ? color : undefined} inverse={focused}>
|
||||
</Text>
|
||||
))}
|
||||
<Text
|
||||
italic
|
||||
bold={focused}
|
||||
dimColor={!focused}
|
||||
color={focused ? color : undefined}
|
||||
inverse={focused}
|
||||
>
|
||||
{focused ? ` ${companion.name} ` : companion.name}
|
||||
</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
|
||||
if (!reaction) {
|
||||
return <Box paddingX={1}>{spriteColumn}</Box>;
|
||||
return <Box paddingX={1}>{spriteColumn}</Box>
|
||||
}
|
||||
|
||||
// Fullscreen: bubble renders separately via CompanionFloatingBubble in
|
||||
@@ -281,90 +289,60 @@ export function CompanionSprite(): React.ReactNode {
|
||||
// Non-fullscreen: bubble sits inline beside the sprite (input shrinks)
|
||||
// because floating into Static scrollback can't be cleared.
|
||||
if (isFullscreenActive()) {
|
||||
return <Box paddingX={1}>{spriteColumn}</Box>;
|
||||
return <Box paddingX={1}>{spriteColumn}</Box>
|
||||
}
|
||||
return <Box flexDirection="row" alignItems="flex-end" paddingX={1} flexShrink={0}>
|
||||
<SpeechBubble text={reaction} color={color} fading={fading} tail="right" />
|
||||
return (
|
||||
<Box flexDirection="row" alignItems="flex-end" paddingX={1} flexShrink={0}>
|
||||
<SpeechBubble
|
||||
text={reaction}
|
||||
color={color}
|
||||
fading={fading}
|
||||
tail="right"
|
||||
/>
|
||||
{spriteColumn}
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Floating bubble overlay for fullscreen mode. Mounted in FullscreenLayout's
|
||||
// bottomFloat slot (outside the overflowY:hidden clip) so it can extend into
|
||||
// the ScrollBox region. CompanionSprite owns the clear-after-10s timer; this
|
||||
// just reads companionReaction and renders the fade.
|
||||
export function CompanionFloatingBubble() {
|
||||
const $ = _c(8);
|
||||
const reaction = useAppState(_temp);
|
||||
let t0;
|
||||
if ($[0] !== reaction) {
|
||||
t0 = {
|
||||
tick: 0,
|
||||
forReaction: reaction
|
||||
};
|
||||
$[0] = reaction;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const [t1, setTick] = useState(t0);
|
||||
const {
|
||||
tick,
|
||||
forReaction
|
||||
} = t1;
|
||||
export function CompanionFloatingBubble(): React.ReactNode {
|
||||
const reaction = useAppState(s => s.companionReaction)
|
||||
const [{ tick, forReaction }, setTick] = useState({
|
||||
tick: 0,
|
||||
forReaction: reaction,
|
||||
})
|
||||
|
||||
// Reset tick synchronously when reaction changes (not in useEffect, which
|
||||
// runs post-render and would show one stale-faded frame). Storing the
|
||||
// reaction the tick is counting FOR alongside the tick itself means the
|
||||
// fade computation never sees a tick from a previous reaction.
|
||||
if (reaction !== forReaction) {
|
||||
setTick({
|
||||
tick: 0,
|
||||
forReaction: reaction
|
||||
});
|
||||
setTick({ tick: 0, forReaction: reaction })
|
||||
}
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[2] !== reaction) {
|
||||
t2 = () => {
|
||||
if (!reaction) {
|
||||
return;
|
||||
}
|
||||
const timer = setInterval(_temp3, TICK_MS, setTick);
|
||||
return () => clearInterval(timer);
|
||||
};
|
||||
t3 = [reaction];
|
||||
$[2] = reaction;
|
||||
$[3] = t2;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
t3 = $[4];
|
||||
}
|
||||
useEffect(t2, t3);
|
||||
if (!feature("BUDDY") || !reaction) {
|
||||
return null;
|
||||
}
|
||||
const companion = getCompanion();
|
||||
if (!companion || getGlobalConfig().companionMuted) {
|
||||
return null;
|
||||
}
|
||||
const t4 = tick >= BUBBLE_SHOW - FADE_WINDOW;
|
||||
let t5;
|
||||
if ($[5] !== reaction || $[6] !== t4) {
|
||||
t5 = <SpeechBubble text={reaction} color={RARITY_COLORS[companion.rarity]} fading={t4} tail="down" />;
|
||||
$[5] = reaction;
|
||||
$[6] = t4;
|
||||
$[7] = t5;
|
||||
} else {
|
||||
t5 = $[7];
|
||||
}
|
||||
return t5;
|
||||
}
|
||||
function _temp3(set) {
|
||||
return set(_temp2);
|
||||
}
|
||||
function _temp2(s_0) {
|
||||
return {
|
||||
...s_0,
|
||||
tick: s_0.tick + 1
|
||||
};
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.companionReaction;
|
||||
|
||||
useEffect(() => {
|
||||
if (!reaction) return
|
||||
const timer = setInterval(
|
||||
set => set(s => ({ ...s, tick: s.tick + 1 })),
|
||||
TICK_MS,
|
||||
setTick,
|
||||
)
|
||||
return () => clearInterval(timer)
|
||||
}, [reaction])
|
||||
|
||||
if (!feature('BUDDY') || !reaction) return null
|
||||
const companion = getCompanion()
|
||||
if (!companion || getGlobalConfig().companionMuted) return null
|
||||
|
||||
return (
|
||||
<SpeechBubble
|
||||
text={reaction}
|
||||
color={RARITY_COLORS[companion.rarity]}
|
||||
fading={tick >= BUBBLE_SHOW - FADE_WINDOW}
|
||||
tail="down"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,97 +1,67 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNotifications } from '../context/notifications.js';
|
||||
import { Text } from '../ink.js';
|
||||
import { getGlobalConfig } from '../utils/config.js';
|
||||
import { getRainbowColor } from '../utils/thinking.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useNotifications } from '../context/notifications.js'
|
||||
import { Text } from '../ink.js'
|
||||
import { getGlobalConfig } from '../utils/config.js'
|
||||
import { getRainbowColor } from '../utils/thinking.js'
|
||||
|
||||
// Local date, not UTC — 24h rolling wave across timezones. Sustained Twitter
|
||||
// buzz instead of a single UTC-midnight spike, gentler on soul-gen load.
|
||||
// Teaser window: April 1-7, 2026 only. Command stays live forever after.
|
||||
export function isBuddyTeaserWindow(): boolean {
|
||||
if ((process.env.USER_TYPE) === 'ant') return true;
|
||||
const d = new Date();
|
||||
return d.getFullYear() === 2026 && d.getMonth() === 3 && d.getDate() <= 7;
|
||||
if (process.env.USER_TYPE === 'ant') return true
|
||||
const d = new Date()
|
||||
return d.getFullYear() === 2026 && d.getMonth() === 3 && d.getDate() <= 7
|
||||
}
|
||||
|
||||
export function isBuddyLive(): boolean {
|
||||
if ((process.env.USER_TYPE) === 'ant') return true;
|
||||
const d = new Date();
|
||||
return d.getFullYear() > 2026 || d.getFullYear() === 2026 && d.getMonth() >= 3;
|
||||
if (process.env.USER_TYPE === 'ant') return true
|
||||
const d = new Date()
|
||||
return (
|
||||
d.getFullYear() > 2026 || (d.getFullYear() === 2026 && d.getMonth() >= 3)
|
||||
)
|
||||
}
|
||||
function RainbowText(t0) {
|
||||
const $ = _c(2);
|
||||
const {
|
||||
text
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== text) {
|
||||
t1 = <>{[...text].map(_temp)}</>;
|
||||
$[0] = text;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
|
||||
function RainbowText({ text }: { text: string }): React.ReactNode {
|
||||
return (
|
||||
<>
|
||||
{[...text].map((ch, i) => (
|
||||
<Text key={i} color={getRainbowColor(i)}>
|
||||
{ch}
|
||||
</Text>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Rainbow /buddy teaser shown on startup when no companion hatched yet.
|
||||
// Idle presence and reactions are handled by CompanionSprite directly.
|
||||
function _temp(ch, i) {
|
||||
return <Text key={i} color={getRainbowColor(i)}>{ch}</Text>;
|
||||
export function useBuddyNotification(): void {
|
||||
const { addNotification, removeNotification } = useNotifications()
|
||||
|
||||
useEffect(() => {
|
||||
if (!feature('BUDDY')) return
|
||||
const config = getGlobalConfig()
|
||||
if (config.companion || !isBuddyTeaserWindow()) return
|
||||
addNotification({
|
||||
key: 'buddy-teaser',
|
||||
jsx: <RainbowText text="/buddy" />,
|
||||
priority: 'immediate',
|
||||
timeoutMs: 15_000,
|
||||
})
|
||||
return () => removeNotification('buddy-teaser')
|
||||
}, [addNotification, removeNotification])
|
||||
}
|
||||
export function useBuddyNotification() {
|
||||
const $ = _c(4);
|
||||
const {
|
||||
addNotification,
|
||||
removeNotification
|
||||
} = useNotifications();
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== addNotification || $[1] !== removeNotification) {
|
||||
t0 = () => {
|
||||
if (!feature("BUDDY")) {
|
||||
return;
|
||||
}
|
||||
const config = getGlobalConfig();
|
||||
if (config.companion || !isBuddyTeaserWindow()) {
|
||||
return;
|
||||
}
|
||||
addNotification({
|
||||
key: "buddy-teaser",
|
||||
jsx: <RainbowText text="/buddy" />,
|
||||
priority: "immediate",
|
||||
timeoutMs: 15000
|
||||
});
|
||||
return () => removeNotification("buddy-teaser");
|
||||
};
|
||||
t1 = [addNotification, removeNotification];
|
||||
$[0] = addNotification;
|
||||
$[1] = removeNotification;
|
||||
$[2] = t0;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t0 = $[2];
|
||||
t1 = $[3];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
}
|
||||
export function findBuddyTriggerPositions(text: string): Array<{
|
||||
start: number;
|
||||
end: number;
|
||||
}> {
|
||||
if (!feature('BUDDY')) return [];
|
||||
const triggers: Array<{
|
||||
start: number;
|
||||
end: number;
|
||||
}> = [];
|
||||
const re = /\/buddy\b/g;
|
||||
let m: RegExpExecArray | null;
|
||||
|
||||
export function findBuddyTriggerPositions(
|
||||
text: string,
|
||||
): Array<{ start: number; end: number }> {
|
||||
if (!feature('BUDDY')) return []
|
||||
const triggers: Array<{ start: number; end: number }> = []
|
||||
const re = /\/buddy\b/g
|
||||
let m: RegExpExecArray | null
|
||||
while ((m = re.exec(text)) !== null) {
|
||||
triggers.push({
|
||||
start: m.index,
|
||||
end: m.index + m[0].length
|
||||
});
|
||||
triggers.push({ start: m.index, end: m.index + m[0].length })
|
||||
}
|
||||
return triggers;
|
||||
return triggers
|
||||
}
|
||||
|
||||
@@ -3,359 +3,453 @@
|
||||
* These are dynamically imported only when the corresponding `claude mcp *` command runs.
|
||||
*/
|
||||
|
||||
import { stat } from 'fs/promises';
|
||||
import pMap from 'p-map';
|
||||
import { cwd } from 'process';
|
||||
import React from 'react';
|
||||
import { MCPServerDesktopImportDialog } from '../../components/MCPServerDesktopImportDialog.js';
|
||||
import { render } from '../../ink.js';
|
||||
import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js';
|
||||
import { clearMcpClientConfig, clearServerTokensFromLocalStorage, getMcpClientConfig, readClientSecret, saveMcpClientSecret } from '../../services/mcp/auth.js';
|
||||
import { connectToServer, getMcpServerConnectionBatchSize } from '../../services/mcp/client.js';
|
||||
import { addMcpConfig, getAllMcpConfigs, getMcpConfigByName, getMcpConfigsByScope, removeMcpConfig } from '../../services/mcp/config.js';
|
||||
import type { ConfigScope, ScopedMcpServerConfig } from '../../services/mcp/types.js';
|
||||
import { describeMcpConfigFilePath, ensureConfigScope, getScopeLabel } from '../../services/mcp/utils.js';
|
||||
import { AppStateProvider } from '../../state/AppState.js';
|
||||
import { getCurrentProjectConfig, getGlobalConfig, saveCurrentProjectConfig } from '../../utils/config.js';
|
||||
import { isFsInaccessible } from '../../utils/errors.js';
|
||||
import { gracefulShutdown } from '../../utils/gracefulShutdown.js';
|
||||
import { safeParseJSON } from '../../utils/json.js';
|
||||
import { getPlatform } from '../../utils/platform.js';
|
||||
import { cliError, cliOk } from '../exit.js';
|
||||
async function checkMcpServerHealth(name: string, server: ScopedMcpServerConfig): Promise<string> {
|
||||
import { stat } from 'fs/promises'
|
||||
import pMap from 'p-map'
|
||||
import { cwd } from 'process'
|
||||
import React from 'react'
|
||||
import { MCPServerDesktopImportDialog } from '../../components/MCPServerDesktopImportDialog.js'
|
||||
import { render } from '../../ink.js'
|
||||
import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
import {
|
||||
clearMcpClientConfig,
|
||||
clearServerTokensFromLocalStorage,
|
||||
getMcpClientConfig,
|
||||
readClientSecret,
|
||||
saveMcpClientSecret,
|
||||
} from '../../services/mcp/auth.js'
|
||||
import {
|
||||
connectToServer,
|
||||
getMcpServerConnectionBatchSize,
|
||||
} from '../../services/mcp/client.js'
|
||||
import {
|
||||
addMcpConfig,
|
||||
getAllMcpConfigs,
|
||||
getMcpConfigByName,
|
||||
getMcpConfigsByScope,
|
||||
removeMcpConfig,
|
||||
} from '../../services/mcp/config.js'
|
||||
import type {
|
||||
ConfigScope,
|
||||
ScopedMcpServerConfig,
|
||||
} from '../../services/mcp/types.js'
|
||||
import {
|
||||
describeMcpConfigFilePath,
|
||||
ensureConfigScope,
|
||||
getScopeLabel,
|
||||
} from '../../services/mcp/utils.js'
|
||||
import { AppStateProvider } from '../../state/AppState.js'
|
||||
import {
|
||||
getCurrentProjectConfig,
|
||||
getGlobalConfig,
|
||||
saveCurrentProjectConfig,
|
||||
} from '../../utils/config.js'
|
||||
import { isFsInaccessible } from '../../utils/errors.js'
|
||||
import { gracefulShutdown } from '../../utils/gracefulShutdown.js'
|
||||
import { safeParseJSON } from '../../utils/json.js'
|
||||
import { getPlatform } from '../../utils/platform.js'
|
||||
import { cliError, cliOk } from '../exit.js'
|
||||
|
||||
async function checkMcpServerHealth(
|
||||
name: string,
|
||||
server: ScopedMcpServerConfig,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const result = await connectToServer(name, server);
|
||||
const result = await connectToServer(name, server)
|
||||
if (result.type === 'connected') {
|
||||
return '✓ Connected';
|
||||
return '✓ Connected'
|
||||
} else if (result.type === 'needs-auth') {
|
||||
return '! Needs authentication';
|
||||
return '! Needs authentication'
|
||||
} else {
|
||||
return '✗ Failed to connect';
|
||||
return '✗ Failed to connect'
|
||||
}
|
||||
} catch (_error) {
|
||||
return '✗ Connection error';
|
||||
return '✗ Connection error'
|
||||
}
|
||||
}
|
||||
|
||||
// mcp serve (lines 4512–4532)
|
||||
export async function mcpServeHandler({
|
||||
debug,
|
||||
verbose
|
||||
verbose,
|
||||
}: {
|
||||
debug?: boolean;
|
||||
verbose?: boolean;
|
||||
debug?: boolean
|
||||
verbose?: boolean
|
||||
}): Promise<void> {
|
||||
const providedCwd = cwd();
|
||||
logEvent('tengu_mcp_start', {});
|
||||
const providedCwd = cwd()
|
||||
logEvent('tengu_mcp_start', {})
|
||||
|
||||
try {
|
||||
await stat(providedCwd);
|
||||
await stat(providedCwd)
|
||||
} catch (error) {
|
||||
if (isFsInaccessible(error)) {
|
||||
cliError(`Error: Directory ${providedCwd} does not exist`);
|
||||
cliError(`Error: Directory ${providedCwd} does not exist`)
|
||||
}
|
||||
throw error;
|
||||
throw error
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
setup
|
||||
} = await import('../../setup.js');
|
||||
await setup(providedCwd, 'default', false, false, undefined, false);
|
||||
const {
|
||||
startMCPServer
|
||||
} = await import('../../entrypoints/mcp.js');
|
||||
await startMCPServer(providedCwd, debug ?? false, verbose ?? false);
|
||||
const { setup } = await import('../../setup.js')
|
||||
await setup(providedCwd, 'default', false, false, undefined, false)
|
||||
const { startMCPServer } = await import('../../entrypoints/mcp.js')
|
||||
await startMCPServer(providedCwd, debug ?? false, verbose ?? false)
|
||||
} catch (error) {
|
||||
cliError(`Error: Failed to start MCP server: ${error}`);
|
||||
cliError(`Error: Failed to start MCP server: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
// mcp remove (lines 4545–4635)
|
||||
export async function mcpRemoveHandler(name: string, options: {
|
||||
scope?: string;
|
||||
}): Promise<void> {
|
||||
export async function mcpRemoveHandler(
|
||||
name: string,
|
||||
options: { scope?: string },
|
||||
): Promise<void> {
|
||||
// Look up config before removing so we can clean up secure storage
|
||||
const serverBeforeRemoval = getMcpConfigByName(name);
|
||||
const serverBeforeRemoval = getMcpConfigByName(name)
|
||||
|
||||
const cleanupSecureStorage = () => {
|
||||
if (serverBeforeRemoval && (serverBeforeRemoval.type === 'sse' || serverBeforeRemoval.type === 'http')) {
|
||||
clearServerTokensFromLocalStorage(name, serverBeforeRemoval);
|
||||
clearMcpClientConfig(name, serverBeforeRemoval);
|
||||
if (
|
||||
serverBeforeRemoval &&
|
||||
(serverBeforeRemoval.type === 'sse' ||
|
||||
serverBeforeRemoval.type === 'http')
|
||||
) {
|
||||
clearServerTokensFromLocalStorage(name, serverBeforeRemoval)
|
||||
clearMcpClientConfig(name, serverBeforeRemoval)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
if (options.scope) {
|
||||
const scope = ensureConfigScope(options.scope);
|
||||
const scope = ensureConfigScope(options.scope)
|
||||
logEvent('tengu_mcp_delete', {
|
||||
name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
scope: scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
await removeMcpConfig(name, scope);
|
||||
cleanupSecureStorage();
|
||||
process.stdout.write(`Removed MCP server ${name} from ${scope} config\n`);
|
||||
cliOk(`File modified: ${describeMcpConfigFilePath(scope)}`);
|
||||
scope:
|
||||
scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
|
||||
await removeMcpConfig(name, scope)
|
||||
cleanupSecureStorage()
|
||||
process.stdout.write(`Removed MCP server ${name} from ${scope} config\n`)
|
||||
cliOk(`File modified: ${describeMcpConfigFilePath(scope)}`)
|
||||
}
|
||||
|
||||
// If no scope specified, check where the server exists
|
||||
const projectConfig = getCurrentProjectConfig();
|
||||
const globalConfig = getGlobalConfig();
|
||||
const projectConfig = getCurrentProjectConfig()
|
||||
const globalConfig = getGlobalConfig()
|
||||
|
||||
// Check if server exists in project scope (.mcp.json)
|
||||
const {
|
||||
servers: projectServers
|
||||
} = getMcpConfigsByScope('project');
|
||||
const mcpJsonExists = !!projectServers[name];
|
||||
const { servers: projectServers } = getMcpConfigsByScope('project')
|
||||
const mcpJsonExists = !!projectServers[name]
|
||||
|
||||
// Count how many scopes contain this server
|
||||
const scopes: Array<Exclude<ConfigScope, 'dynamic'>> = [];
|
||||
if (projectConfig.mcpServers?.[name]) scopes.push('local');
|
||||
if (mcpJsonExists) scopes.push('project');
|
||||
if (globalConfig.mcpServers?.[name]) scopes.push('user');
|
||||
const scopes: Array<Exclude<ConfigScope, 'dynamic'>> = []
|
||||
if (projectConfig.mcpServers?.[name]) scopes.push('local')
|
||||
if (mcpJsonExists) scopes.push('project')
|
||||
if (globalConfig.mcpServers?.[name]) scopes.push('user')
|
||||
|
||||
if (scopes.length === 0) {
|
||||
cliError(`No MCP server found with name: "${name}"`);
|
||||
cliError(`No MCP server found with name: "${name}"`)
|
||||
} else if (scopes.length === 1) {
|
||||
// Server exists in only one scope, remove it
|
||||
const scope = scopes[0]!;
|
||||
const scope = scopes[0]!
|
||||
logEvent('tengu_mcp_delete', {
|
||||
name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
scope: scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
await removeMcpConfig(name, scope);
|
||||
cleanupSecureStorage();
|
||||
process.stdout.write(`Removed MCP server "${name}" from ${scope} config\n`);
|
||||
cliOk(`File modified: ${describeMcpConfigFilePath(scope)}`);
|
||||
scope:
|
||||
scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
|
||||
await removeMcpConfig(name, scope)
|
||||
cleanupSecureStorage()
|
||||
process.stdout.write(
|
||||
`Removed MCP server "${name}" from ${scope} config\n`,
|
||||
)
|
||||
cliOk(`File modified: ${describeMcpConfigFilePath(scope)}`)
|
||||
} else {
|
||||
// Server exists in multiple scopes
|
||||
process.stderr.write(`MCP server "${name}" exists in multiple scopes:\n`);
|
||||
process.stderr.write(`MCP server "${name}" exists in multiple scopes:\n`)
|
||||
scopes.forEach(scope => {
|
||||
process.stderr.write(` - ${getScopeLabel(scope)} (${describeMcpConfigFilePath(scope)})\n`);
|
||||
});
|
||||
process.stderr.write('\nTo remove from a specific scope, use:\n');
|
||||
process.stderr.write(
|
||||
` - ${getScopeLabel(scope)} (${describeMcpConfigFilePath(scope)})\n`,
|
||||
)
|
||||
})
|
||||
process.stderr.write('\nTo remove from a specific scope, use:\n')
|
||||
scopes.forEach(scope => {
|
||||
process.stderr.write(` claude mcp remove "${name}" -s ${scope}\n`);
|
||||
});
|
||||
cliError();
|
||||
process.stderr.write(` claude mcp remove "${name}" -s ${scope}\n`)
|
||||
})
|
||||
cliError()
|
||||
}
|
||||
} catch (error) {
|
||||
cliError((error as Error).message);
|
||||
cliError((error as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
// mcp list (lines 4641–4688)
|
||||
export async function mcpListHandler(): Promise<void> {
|
||||
logEvent('tengu_mcp_list', {});
|
||||
const {
|
||||
servers: configs
|
||||
} = await getAllMcpConfigs();
|
||||
logEvent('tengu_mcp_list', {})
|
||||
const { servers: configs } = await getAllMcpConfigs()
|
||||
if (Object.keys(configs).length === 0) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('No MCP servers configured. Use `claude mcp add` to add a server.');
|
||||
console.log(
|
||||
'No MCP servers configured. Use `claude mcp add` to add a server.',
|
||||
)
|
||||
} else {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('Checking MCP server health...\n');
|
||||
console.log('Checking MCP server health...\n')
|
||||
|
||||
// Check servers concurrently
|
||||
const entries = Object.entries(configs);
|
||||
const results = await pMap(entries, async ([name, server]) => ({
|
||||
name,
|
||||
server,
|
||||
status: await checkMcpServerHealth(name, server)
|
||||
}), {
|
||||
concurrency: getMcpServerConnectionBatchSize()
|
||||
});
|
||||
for (const {
|
||||
name,
|
||||
server,
|
||||
status
|
||||
} of results) {
|
||||
const entries = Object.entries(configs)
|
||||
const results = await pMap(
|
||||
entries,
|
||||
async ([name, server]) => ({
|
||||
name,
|
||||
server,
|
||||
status: await checkMcpServerHealth(name, server),
|
||||
}),
|
||||
{ concurrency: getMcpServerConnectionBatchSize() },
|
||||
)
|
||||
|
||||
for (const { name, server, status } of results) {
|
||||
// Intentionally excluding sse-ide servers here since they're internal
|
||||
if (server.type === 'sse') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`${name}: ${server.url} (SSE) - ${status}`);
|
||||
console.log(`${name}: ${server.url} (SSE) - ${status}`)
|
||||
} else if (server.type === 'http') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`${name}: ${server.url} (HTTP) - ${status}`);
|
||||
console.log(`${name}: ${server.url} (HTTP) - ${status}`)
|
||||
} else if (server.type === 'claudeai-proxy') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`${name}: ${server.url} - ${status}`);
|
||||
console.log(`${name}: ${server.url} - ${status}`)
|
||||
} else if (!server.type || server.type === 'stdio') {
|
||||
const args = Array.isArray((server as any).args) ? (server as any).args : [];
|
||||
const args = Array.isArray(server.args) ? server.args : []
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`${name}: ${(server as any).command} ${args.join(' ')} - ${status}`);
|
||||
console.log(`${name}: ${server.command} ${args.join(' ')} - ${status}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Use gracefulShutdown to properly clean up MCP server connections
|
||||
// (process.exit bypasses cleanup handlers, leaving child processes orphaned)
|
||||
await gracefulShutdown(0);
|
||||
await gracefulShutdown(0)
|
||||
}
|
||||
|
||||
// mcp get (lines 4694–4786)
|
||||
export async function mcpGetHandler(name: string): Promise<void> {
|
||||
logEvent('tengu_mcp_get', {
|
||||
name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
const server = getMcpConfigByName(name);
|
||||
name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
const server = getMcpConfigByName(name)
|
||||
if (!server) {
|
||||
cliError(`No MCP server found with name: ${name}`);
|
||||
cliError(`No MCP server found with name: ${name}`)
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`${name}:`);
|
||||
console.log(`${name}:`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Scope: ${getScopeLabel(server.scope)}`);
|
||||
console.log(` Scope: ${getScopeLabel(server.scope)}`)
|
||||
|
||||
// Check server health
|
||||
const status = await checkMcpServerHealth(name, server);
|
||||
const status = await checkMcpServerHealth(name, server)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Status: ${status}`);
|
||||
console.log(` Status: ${status}`)
|
||||
|
||||
// Intentionally excluding sse-ide servers here since they're internal
|
||||
if (server.type === 'sse') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Type: sse`);
|
||||
console.log(` Type: sse`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` URL: ${server.url}`);
|
||||
console.log(` URL: ${server.url}`)
|
||||
if (server.headers) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(' Headers:');
|
||||
console.log(' Headers:')
|
||||
for (const [key, value] of Object.entries(server.headers)) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` ${key}: ${value}`);
|
||||
console.log(` ${key}: ${value}`)
|
||||
}
|
||||
}
|
||||
if (server.oauth?.clientId || server.oauth?.callbackPort) {
|
||||
const parts: string[] = [];
|
||||
const parts: string[] = []
|
||||
if (server.oauth.clientId) {
|
||||
parts.push('client_id configured');
|
||||
const clientConfig = getMcpClientConfig(name, server);
|
||||
if (clientConfig?.clientSecret) parts.push('client_secret configured');
|
||||
parts.push('client_id configured')
|
||||
const clientConfig = getMcpClientConfig(name, server)
|
||||
if (clientConfig?.clientSecret) parts.push('client_secret configured')
|
||||
}
|
||||
if (server.oauth.callbackPort) parts.push(`callback_port ${server.oauth.callbackPort}`);
|
||||
if (server.oauth.callbackPort)
|
||||
parts.push(`callback_port ${server.oauth.callbackPort}`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` OAuth: ${parts.join(', ')}`);
|
||||
console.log(` OAuth: ${parts.join(', ')}`)
|
||||
}
|
||||
} else if (server.type === 'http') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Type: http`);
|
||||
console.log(` Type: http`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` URL: ${server.url}`);
|
||||
console.log(` URL: ${server.url}`)
|
||||
if (server.headers) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(' Headers:');
|
||||
console.log(' Headers:')
|
||||
for (const [key, value] of Object.entries(server.headers)) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` ${key}: ${value}`);
|
||||
console.log(` ${key}: ${value}`)
|
||||
}
|
||||
}
|
||||
if (server.oauth?.clientId || server.oauth?.callbackPort) {
|
||||
const parts: string[] = [];
|
||||
const parts: string[] = []
|
||||
if (server.oauth.clientId) {
|
||||
parts.push('client_id configured');
|
||||
const clientConfig = getMcpClientConfig(name, server);
|
||||
if (clientConfig?.clientSecret) parts.push('client_secret configured');
|
||||
parts.push('client_id configured')
|
||||
const clientConfig = getMcpClientConfig(name, server)
|
||||
if (clientConfig?.clientSecret) parts.push('client_secret configured')
|
||||
}
|
||||
if (server.oauth.callbackPort) parts.push(`callback_port ${server.oauth.callbackPort}`);
|
||||
if (server.oauth.callbackPort)
|
||||
parts.push(`callback_port ${server.oauth.callbackPort}`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` OAuth: ${parts.join(', ')}`);
|
||||
console.log(` OAuth: ${parts.join(', ')}`)
|
||||
}
|
||||
} else if (server.type === 'stdio') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Type: stdio`);
|
||||
console.log(` Type: stdio`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Command: ${server.command}`);
|
||||
const args = Array.isArray(server.args) ? server.args : [];
|
||||
console.log(` Command: ${server.command}`)
|
||||
const args = Array.isArray(server.args) ? server.args : []
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Args: ${args.join(' ')}`);
|
||||
console.log(` Args: ${args.join(' ')}`)
|
||||
if (server.env) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(' Environment:');
|
||||
console.log(' Environment:')
|
||||
for (const [key, value] of Object.entries(server.env)) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` ${key}=${value}`);
|
||||
console.log(` ${key}=${value}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`\nTo remove this server, run: claude mcp remove "${name}" -s ${server.scope}`);
|
||||
console.log(
|
||||
`\nTo remove this server, run: claude mcp remove "${name}" -s ${server.scope}`,
|
||||
)
|
||||
// Use gracefulShutdown to properly clean up MCP server connections
|
||||
// (process.exit bypasses cleanup handlers, leaving child processes orphaned)
|
||||
await gracefulShutdown(0);
|
||||
await gracefulShutdown(0)
|
||||
}
|
||||
|
||||
// mcp add-json (lines 4801–4870)
|
||||
export async function mcpAddJsonHandler(name: string, json: string, options: {
|
||||
scope?: string;
|
||||
clientSecret?: true;
|
||||
}): Promise<void> {
|
||||
export async function mcpAddJsonHandler(
|
||||
name: string,
|
||||
json: string,
|
||||
options: { scope?: string; clientSecret?: true },
|
||||
): Promise<void> {
|
||||
try {
|
||||
const scope = ensureConfigScope(options.scope);
|
||||
const parsedJson = safeParseJSON(json);
|
||||
const scope = ensureConfigScope(options.scope)
|
||||
const parsedJson = safeParseJSON(json)
|
||||
|
||||
// Read secret before writing config so cancellation doesn't leave partial state
|
||||
const needsSecret = options.clientSecret && parsedJson && typeof parsedJson === 'object' && 'type' in parsedJson && (parsedJson.type === 'sse' || parsedJson.type === 'http') && 'url' in parsedJson && typeof parsedJson.url === 'string' && 'oauth' in parsedJson && parsedJson.oauth && typeof parsedJson.oauth === 'object' && 'clientId' in parsedJson.oauth;
|
||||
const clientSecret = needsSecret ? await readClientSecret() : undefined;
|
||||
await addMcpConfig(name, parsedJson, scope);
|
||||
const transportType = parsedJson && typeof parsedJson === 'object' && 'type' in parsedJson ? String(parsedJson.type || 'stdio') : 'stdio';
|
||||
if (clientSecret && parsedJson && typeof parsedJson === 'object' && 'type' in parsedJson && (parsedJson.type === 'sse' || parsedJson.type === 'http') && 'url' in parsedJson && typeof parsedJson.url === 'string') {
|
||||
saveMcpClientSecret(name, {
|
||||
type: parsedJson.type,
|
||||
url: parsedJson.url
|
||||
}, clientSecret);
|
||||
const needsSecret =
|
||||
options.clientSecret &&
|
||||
parsedJson &&
|
||||
typeof parsedJson === 'object' &&
|
||||
'type' in parsedJson &&
|
||||
(parsedJson.type === 'sse' || parsedJson.type === 'http') &&
|
||||
'url' in parsedJson &&
|
||||
typeof parsedJson.url === 'string' &&
|
||||
'oauth' in parsedJson &&
|
||||
parsedJson.oauth &&
|
||||
typeof parsedJson.oauth === 'object' &&
|
||||
'clientId' in parsedJson.oauth
|
||||
const clientSecret = needsSecret ? await readClientSecret() : undefined
|
||||
|
||||
await addMcpConfig(name, parsedJson, scope)
|
||||
|
||||
const transportType =
|
||||
parsedJson && typeof parsedJson === 'object' && 'type' in parsedJson
|
||||
? String(parsedJson.type || 'stdio')
|
||||
: 'stdio'
|
||||
|
||||
if (
|
||||
clientSecret &&
|
||||
parsedJson &&
|
||||
typeof parsedJson === 'object' &&
|
||||
'type' in parsedJson &&
|
||||
(parsedJson.type === 'sse' || parsedJson.type === 'http') &&
|
||||
'url' in parsedJson &&
|
||||
typeof parsedJson.url === 'string'
|
||||
) {
|
||||
saveMcpClientSecret(
|
||||
name,
|
||||
{ type: parsedJson.type, url: parsedJson.url },
|
||||
clientSecret,
|
||||
)
|
||||
}
|
||||
|
||||
logEvent('tengu_mcp_add', {
|
||||
scope: scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
source: 'json' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
type: transportType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
cliOk(`Added ${transportType} MCP server ${name} to ${scope} config`);
|
||||
scope:
|
||||
scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
source:
|
||||
'json' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
type: transportType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
|
||||
cliOk(`Added ${transportType} MCP server ${name} to ${scope} config`)
|
||||
} catch (error) {
|
||||
cliError((error as Error).message);
|
||||
cliError((error as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
// mcp add-from-claude-desktop (lines 4881–4927)
|
||||
export async function mcpAddFromDesktopHandler(options: {
|
||||
scope?: string;
|
||||
scope?: string
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const scope = ensureConfigScope(options.scope);
|
||||
const platform = getPlatform();
|
||||
const scope = ensureConfigScope(options.scope)
|
||||
const platform = getPlatform()
|
||||
|
||||
logEvent('tengu_mcp_add', {
|
||||
scope: scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
platform: platform as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
source: 'desktop' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
const {
|
||||
readClaudeDesktopMcpServers
|
||||
} = await import('../../utils/claudeDesktop.js');
|
||||
const servers = await readClaudeDesktopMcpServers();
|
||||
scope:
|
||||
scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
platform:
|
||||
platform as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
source:
|
||||
'desktop' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
|
||||
const { readClaudeDesktopMcpServers } = await import(
|
||||
'../../utils/claudeDesktop.js'
|
||||
)
|
||||
const servers = await readClaudeDesktopMcpServers()
|
||||
|
||||
if (Object.keys(servers).length === 0) {
|
||||
cliOk('No MCP servers found in Claude Desktop configuration or configuration file does not exist.');
|
||||
cliOk(
|
||||
'No MCP servers found in Claude Desktop configuration or configuration file does not exist.',
|
||||
)
|
||||
}
|
||||
const {
|
||||
unmount
|
||||
} = await render(<AppStateProvider>
|
||||
|
||||
const { unmount } = await render(
|
||||
<AppStateProvider>
|
||||
<KeybindingSetup>
|
||||
<MCPServerDesktopImportDialog servers={servers} scope={scope} onDone={() => {
|
||||
unmount();
|
||||
}} />
|
||||
<MCPServerDesktopImportDialog
|
||||
servers={servers}
|
||||
scope={scope}
|
||||
onDone={() => {
|
||||
unmount()
|
||||
}}
|
||||
/>
|
||||
</KeybindingSetup>
|
||||
</AppStateProvider>, {
|
||||
exitOnCtrlC: true
|
||||
});
|
||||
</AppStateProvider>,
|
||||
{ exitOnCtrlC: true },
|
||||
)
|
||||
} catch (error) {
|
||||
cliError((error as Error).message);
|
||||
cliError((error as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
// mcp reset-project-choices (lines 4935–4952)
|
||||
export async function mcpResetChoicesHandler(): Promise<void> {
|
||||
logEvent('tengu_mcp_reset_mcpjson_choices', {});
|
||||
logEvent('tengu_mcp_reset_mcpjson_choices', {})
|
||||
saveCurrentProjectConfig(current => ({
|
||||
...current,
|
||||
enabledMcpjsonServers: [],
|
||||
disabledMcpjsonServers: [],
|
||||
enableAllProjectMcpServers: false
|
||||
}));
|
||||
cliOk('All project-scoped (.mcp.json) server approvals and rejections have been reset.\n' + 'You will be prompted for approval next time you start Claude Code.');
|
||||
enableAllProjectMcpServers: false,
|
||||
}))
|
||||
cliOk(
|
||||
'All project-scoped (.mcp.json) server approvals and rejections have been reset.\n' +
|
||||
'You will be prompted for approval next time you start Claude Code.',
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,34 +1,37 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
/**
|
||||
* Miscellaneous subcommand handlers — extracted from main.tsx for lazy loading.
|
||||
* setup-token, doctor, install
|
||||
*/
|
||||
/* eslint-disable custom-rules/no-process-exit -- CLI subcommand handlers intentionally exit */
|
||||
|
||||
import { cwd } from 'process';
|
||||
import React from 'react';
|
||||
import { WelcomeV2 } from '../../components/LogoV2/WelcomeV2.js';
|
||||
import { useManagePlugins } from '../../hooks/useManagePlugins.js';
|
||||
import type { Root } from '../../ink.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js';
|
||||
import { logEvent } from '../../services/analytics/index.js';
|
||||
import { MCPConnectionManager } from '../../services/mcp/MCPConnectionManager.js';
|
||||
import { AppStateProvider } from '../../state/AppState.js';
|
||||
import { onChangeAppState } from '../../state/onChangeAppState.js';
|
||||
import { isAnthropicAuthEnabled } from '../../utils/auth.js';
|
||||
import { cwd } from 'process'
|
||||
import React from 'react'
|
||||
import { WelcomeV2 } from '../../components/LogoV2/WelcomeV2.js'
|
||||
import { useManagePlugins } from '../../hooks/useManagePlugins.js'
|
||||
import type { Root } from '../../ink.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js'
|
||||
import { logEvent } from '../../services/analytics/index.js'
|
||||
import { MCPConnectionManager } from '../../services/mcp/MCPConnectionManager.js'
|
||||
import { AppStateProvider } from '../../state/AppState.js'
|
||||
import { onChangeAppState } from '../../state/onChangeAppState.js'
|
||||
import { isAnthropicAuthEnabled } from '../../utils/auth.js'
|
||||
|
||||
export async function setupTokenHandler(root: Root): Promise<void> {
|
||||
logEvent('tengu_setup_token_command', {});
|
||||
const showAuthWarning = !isAnthropicAuthEnabled();
|
||||
const {
|
||||
ConsoleOAuthFlow
|
||||
} = await import('../../components/ConsoleOAuthFlow.js');
|
||||
logEvent('tengu_setup_token_command', {})
|
||||
|
||||
const showAuthWarning = !isAnthropicAuthEnabled()
|
||||
const { ConsoleOAuthFlow } = await import(
|
||||
'../../components/ConsoleOAuthFlow.js'
|
||||
)
|
||||
await new Promise<void>(resolve => {
|
||||
root.render(<AppStateProvider onChangeAppState={onChangeAppState}>
|
||||
root.render(
|
||||
<AppStateProvider onChangeAppState={onChangeAppState}>
|
||||
<KeybindingSetup>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<WelcomeV2 />
|
||||
{showAuthWarning && <Box flexDirection="column">
|
||||
{showAuthWarning && (
|
||||
<Box flexDirection="column">
|
||||
<Text color="warning">
|
||||
Warning: You already have authentication configured via
|
||||
environment variable or API key helper.
|
||||
@@ -37,73 +40,87 @@ export async function setupTokenHandler(root: Root): Promise<void> {
|
||||
The setup-token command will create a new OAuth token which
|
||||
you can use instead.
|
||||
</Text>
|
||||
</Box>}
|
||||
<ConsoleOAuthFlow onDone={() => {
|
||||
void resolve();
|
||||
}} mode="setup-token" startingMessage="This will guide you through long-lived (1-year) auth token setup for your Claude account. Claude subscription required." />
|
||||
</Box>
|
||||
)}
|
||||
<ConsoleOAuthFlow
|
||||
onDone={() => {
|
||||
void resolve()
|
||||
}}
|
||||
mode="setup-token"
|
||||
startingMessage="This will guide you through long-lived (1-year) auth token setup for your Claude account. Claude subscription required."
|
||||
/>
|
||||
</Box>
|
||||
</KeybindingSetup>
|
||||
</AppStateProvider>);
|
||||
});
|
||||
root.unmount();
|
||||
process.exit(0);
|
||||
</AppStateProvider>,
|
||||
)
|
||||
})
|
||||
root.unmount()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// DoctorWithPlugins wrapper + doctor handler
|
||||
const DoctorLazy = React.lazy(() => import('../../screens/Doctor.js').then(m => ({
|
||||
default: m.Doctor
|
||||
})));
|
||||
function DoctorWithPlugins(t0) {
|
||||
const $ = _c(2);
|
||||
const {
|
||||
onDone
|
||||
} = t0;
|
||||
useManagePlugins();
|
||||
let t1;
|
||||
if ($[0] !== onDone) {
|
||||
t1 = <React.Suspense fallback={null}><DoctorLazy onDone={onDone} /></React.Suspense>;
|
||||
$[0] = onDone;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
const DoctorLazy = React.lazy(() =>
|
||||
import('../../screens/Doctor.js').then(m => ({ default: m.Doctor })),
|
||||
)
|
||||
|
||||
function DoctorWithPlugins({
|
||||
onDone,
|
||||
}: {
|
||||
onDone: () => void
|
||||
}): React.ReactNode {
|
||||
useManagePlugins()
|
||||
return (
|
||||
<React.Suspense fallback={null}>
|
||||
<DoctorLazy onDone={onDone} />
|
||||
</React.Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
export async function doctorHandler(root: Root): Promise<void> {
|
||||
logEvent('tengu_doctor_command', {});
|
||||
logEvent('tengu_doctor_command', {})
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
root.render(<AppStateProvider>
|
||||
root.render(
|
||||
<AppStateProvider>
|
||||
<KeybindingSetup>
|
||||
<MCPConnectionManager dynamicMcpConfig={undefined} isStrictMcpConfig={false}>
|
||||
<DoctorWithPlugins onDone={() => {
|
||||
void resolve();
|
||||
}} />
|
||||
<MCPConnectionManager
|
||||
dynamicMcpConfig={undefined}
|
||||
isStrictMcpConfig={false}
|
||||
>
|
||||
<DoctorWithPlugins
|
||||
onDone={() => {
|
||||
void resolve()
|
||||
}}
|
||||
/>
|
||||
</MCPConnectionManager>
|
||||
</KeybindingSetup>
|
||||
</AppStateProvider>);
|
||||
});
|
||||
root.unmount();
|
||||
process.exit(0);
|
||||
</AppStateProvider>,
|
||||
)
|
||||
})
|
||||
root.unmount()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// install handler
|
||||
export async function installHandler(target: string | undefined, options: {
|
||||
force?: boolean;
|
||||
}): Promise<void> {
|
||||
const {
|
||||
setup
|
||||
} = await import('../../setup.js');
|
||||
await setup(cwd(), 'default', false, false, undefined, false);
|
||||
const {
|
||||
install
|
||||
} = await import('../../commands/install.js');
|
||||
export async function installHandler(
|
||||
target: string | undefined,
|
||||
options: { force?: boolean },
|
||||
): Promise<void> {
|
||||
const { setup } = await import('../../setup.js')
|
||||
await setup(cwd(), 'default', false, false, undefined, false)
|
||||
const { install } = await import('../../commands/install.js')
|
||||
await new Promise<void>(resolve => {
|
||||
const args: string[] = [];
|
||||
if (target) args.push(target);
|
||||
if (options.force) args.push('--force');
|
||||
void install.call(result => {
|
||||
void resolve();
|
||||
process.exit(result.includes('failed') ? 1 : 0);
|
||||
}, {}, args);
|
||||
});
|
||||
const args: string[] = []
|
||||
if (target) args.push(target)
|
||||
if (options.force) args.push('--force')
|
||||
|
||||
void install.call(
|
||||
result => {
|
||||
void resolve()
|
||||
process.exit(result.includes('failed') ? 1 : 0)
|
||||
},
|
||||
{},
|
||||
args,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,125 +1,154 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import chalk from 'chalk';
|
||||
import figures from 'figures';
|
||||
import React, { useEffect } from 'react';
|
||||
import { getAdditionalDirectoriesForClaudeMd, setAdditionalDirectoriesForClaudeMd } from '../../bootstrap/state.js';
|
||||
import type { LocalJSXCommandContext } from '../../commands.js';
|
||||
import { MessageResponse } from '../../components/MessageResponse.js';
|
||||
import { AddWorkspaceDirectory } from '../../components/permissions/rules/AddWorkspaceDirectory.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { applyPermissionUpdate, persistPermissionUpdate } from '../../utils/permissions/PermissionUpdate.js';
|
||||
import type { PermissionUpdateDestination } from '../../utils/permissions/PermissionUpdateSchema.js';
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js';
|
||||
import { addDirHelpMessage, validateDirectoryForWorkspace } from './validation.js';
|
||||
function AddDirError(t0) {
|
||||
const $ = _c(10);
|
||||
const {
|
||||
message,
|
||||
args,
|
||||
onDone
|
||||
} = t0;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== onDone) {
|
||||
t1 = () => {
|
||||
const timer = setTimeout(onDone, 0);
|
||||
return () => clearTimeout(timer);
|
||||
};
|
||||
t2 = [onDone];
|
||||
$[0] = onDone;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== args) {
|
||||
t3 = <Text dimColor={true}>{figures.pointer} /add-dir {args}</Text>;
|
||||
$[3] = args;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== message) {
|
||||
t4 = <MessageResponse><Text>{message}</Text></MessageResponse>;
|
||||
$[5] = message;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
let t5;
|
||||
if ($[7] !== t3 || $[8] !== t4) {
|
||||
t5 = <Box flexDirection="column">{t3}{t4}</Box>;
|
||||
$[7] = t3;
|
||||
$[8] = t4;
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
return t5;
|
||||
import chalk from 'chalk'
|
||||
import figures from 'figures'
|
||||
import React, { useEffect } from 'react'
|
||||
import {
|
||||
getAdditionalDirectoriesForClaudeMd,
|
||||
setAdditionalDirectoriesForClaudeMd,
|
||||
} from '../../bootstrap/state.js'
|
||||
import type { LocalJSXCommandContext } from '../../commands.js'
|
||||
import { MessageResponse } from '../../components/MessageResponse.js'
|
||||
import { AddWorkspaceDirectory } from '../../components/permissions/rules/AddWorkspaceDirectory.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import {
|
||||
applyPermissionUpdate,
|
||||
persistPermissionUpdate,
|
||||
} from '../../utils/permissions/PermissionUpdate.js'
|
||||
import type { PermissionUpdateDestination } from '../../utils/permissions/PermissionUpdateSchema.js'
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
|
||||
import {
|
||||
addDirHelpMessage,
|
||||
validateDirectoryForWorkspace,
|
||||
} from './validation.js'
|
||||
|
||||
function AddDirError({
|
||||
message,
|
||||
args,
|
||||
onDone,
|
||||
}: {
|
||||
message: string
|
||||
args: string
|
||||
onDone: () => void
|
||||
}): React.ReactNode {
|
||||
useEffect(() => {
|
||||
// We need to defer calling onDone to avoid the "return null" bug where
|
||||
// the component unmounts before React can render the error message.
|
||||
// Using setTimeout ensures the error displays before the command exits.
|
||||
const timer = setTimeout(onDone, 0)
|
||||
return () => clearTimeout(timer)
|
||||
}, [onDone])
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor>
|
||||
{figures.pointer} /add-dir {args}
|
||||
</Text>
|
||||
<MessageResponse>
|
||||
<Text>{message}</Text>
|
||||
</MessageResponse>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext, args?: string): Promise<React.ReactNode> {
|
||||
const directoryPath = (args ?? '').trim();
|
||||
const appState = context.getAppState();
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
args?: string,
|
||||
): Promise<React.ReactNode> {
|
||||
const directoryPath = (args ?? '').trim()
|
||||
const appState = context.getAppState()
|
||||
|
||||
// Helper to handle adding a directory (shared by both with-path and no-path cases)
|
||||
const handleAddDirectory = async (path: string, remember = false) => {
|
||||
const destination: PermissionUpdateDestination = remember ? 'localSettings' : 'session';
|
||||
const destination: PermissionUpdateDestination = remember
|
||||
? 'localSettings'
|
||||
: 'session'
|
||||
|
||||
const permissionUpdate = {
|
||||
type: 'addDirectories' as const,
|
||||
directories: [path],
|
||||
destination
|
||||
};
|
||||
destination,
|
||||
}
|
||||
|
||||
// Apply to session context
|
||||
const latestAppState = context.getAppState();
|
||||
const updatedContext = applyPermissionUpdate(latestAppState.toolPermissionContext, permissionUpdate);
|
||||
const latestAppState = context.getAppState()
|
||||
const updatedContext = applyPermissionUpdate(
|
||||
latestAppState.toolPermissionContext,
|
||||
permissionUpdate,
|
||||
)
|
||||
context.setAppState(prev => ({
|
||||
...prev,
|
||||
toolPermissionContext: updatedContext
|
||||
}));
|
||||
toolPermissionContext: updatedContext,
|
||||
}))
|
||||
|
||||
// Update sandbox config so Bash commands can access the new directory.
|
||||
// Bootstrap state is the source of truth for session-only dirs; persisted
|
||||
// dirs are picked up via the settings subscription, but we refresh
|
||||
// eagerly here to avoid a race when the user acts immediately.
|
||||
const currentDirs = getAdditionalDirectoriesForClaudeMd();
|
||||
const currentDirs = getAdditionalDirectoriesForClaudeMd()
|
||||
if (!currentDirs.includes(path)) {
|
||||
setAdditionalDirectoriesForClaudeMd([...currentDirs, path]);
|
||||
setAdditionalDirectoriesForClaudeMd([...currentDirs, path])
|
||||
}
|
||||
SandboxManager.refreshConfig();
|
||||
let message: string;
|
||||
SandboxManager.refreshConfig()
|
||||
|
||||
let message: string
|
||||
|
||||
if (remember) {
|
||||
try {
|
||||
persistPermissionUpdate(permissionUpdate);
|
||||
message = `Added ${chalk.bold(path)} as a working directory and saved to local settings`;
|
||||
persistPermissionUpdate(permissionUpdate)
|
||||
message = `Added ${chalk.bold(path)} as a working directory and saved to local settings`
|
||||
} catch (error) {
|
||||
message = `Added ${chalk.bold(path)} as a working directory. Failed to save to local settings: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
||||
message = `Added ${chalk.bold(path)} as a working directory. Failed to save to local settings: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
}
|
||||
} else {
|
||||
message = `Added ${chalk.bold(path)} as a working directory for this session`;
|
||||
message = `Added ${chalk.bold(path)} as a working directory for this session`
|
||||
}
|
||||
const messageWithHint = `${message} ${chalk.dim('· /permissions to manage')}`;
|
||||
onDone(messageWithHint);
|
||||
};
|
||||
|
||||
const messageWithHint = `${message} ${chalk.dim('· /permissions to manage')}`
|
||||
onDone(messageWithHint)
|
||||
}
|
||||
|
||||
// When no path is provided, show AddWorkspaceDirectory input form directly
|
||||
// and return to REPL after confirmation
|
||||
if (!directoryPath) {
|
||||
return <AddWorkspaceDirectory permissionContext={appState.toolPermissionContext} onAddDirectory={handleAddDirectory} onCancel={() => {
|
||||
onDone('Did not add a working directory.');
|
||||
}} />;
|
||||
return (
|
||||
<AddWorkspaceDirectory
|
||||
permissionContext={appState.toolPermissionContext}
|
||||
onAddDirectory={handleAddDirectory}
|
||||
onCancel={() => {
|
||||
onDone('Did not add a working directory.')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const result = await validateDirectoryForWorkspace(directoryPath, appState.toolPermissionContext);
|
||||
|
||||
const result = await validateDirectoryForWorkspace(
|
||||
directoryPath,
|
||||
appState.toolPermissionContext,
|
||||
)
|
||||
|
||||
if (result.resultType !== 'success') {
|
||||
const message = addDirHelpMessage(result);
|
||||
return <AddDirError message={message} args={args ?? ''} onDone={() => onDone(message)} />;
|
||||
const message = addDirHelpMessage(result)
|
||||
|
||||
return (
|
||||
<AddDirError
|
||||
message={message}
|
||||
args={args ?? ''}
|
||||
onDone={() => onDone(message)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <AddWorkspaceDirectory directoryPath={result.absolutePath} permissionContext={appState.toolPermissionContext} onAddDirectory={handleAddDirectory} onCancel={() => {
|
||||
onDone(`Did not add ${chalk.bold(result.absolutePath)} as a working directory.`);
|
||||
}} />;
|
||||
|
||||
return (
|
||||
<AddWorkspaceDirectory
|
||||
directoryPath={result.absolutePath}
|
||||
permissionContext={appState.toolPermissionContext}
|
||||
onAddDirectory={handleAddDirectory}
|
||||
onCancel={() => {
|
||||
onDone(
|
||||
`Did not add ${chalk.bold(result.absolutePath)} as a working directory.`,
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import * as React from 'react';
|
||||
import { AgentsMenu } from '../../components/agents/AgentsMenu.js';
|
||||
import type { ToolUseContext } from '../../Tool.js';
|
||||
import { getTools } from '../../tools.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: ToolUseContext): Promise<React.ReactNode> {
|
||||
const appState = context.getAppState();
|
||||
const permissionContext = appState.toolPermissionContext;
|
||||
const tools = getTools(permissionContext);
|
||||
return <AgentsMenu tools={tools} onExit={onDone} />;
|
||||
import * as React from 'react'
|
||||
import { AgentsMenu } from '../../components/agents/AgentsMenu.js'
|
||||
import type { ToolUseContext } from '../../Tool.js'
|
||||
import { getTools } from '../../tools.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: ToolUseContext,
|
||||
): Promise<React.ReactNode> {
|
||||
const appState = context.getAppState()
|
||||
const permissionContext = appState.toolPermissionContext
|
||||
const tools = getTools(permissionContext)
|
||||
|
||||
return <AgentsMenu tools={tools} onExit={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,27 +1,40 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import { toString as qrToString } from 'qrcode';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getBridgeAccessToken } from '../../bridge/bridgeConfig.js';
|
||||
import { checkBridgeMinVersion, getBridgeDisabledReason, isEnvLessBridgeEnabled } from '../../bridge/bridgeEnabled.js';
|
||||
import { checkEnvLessBridgeMinVersion } from '../../bridge/envLessBridgeConfig.js';
|
||||
import { BRIDGE_LOGIN_INSTRUCTION, REMOTE_CONTROL_DISCONNECTED_MSG } from '../../bridge/types.js';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { ListItem } from '../../components/design-system/ListItem.js';
|
||||
import { shouldShowRemoteCallout } from '../../components/RemoteCallout.js';
|
||||
import { useRegisterOverlay } from '../../context/overlayContext.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js';
|
||||
import { useAppState, useSetAppState } from '../../state/AppState.js';
|
||||
import type { ToolUseContext } from '../../Tool.js';
|
||||
import type { LocalJSXCommandContext, LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { logForDebugging } from '../../utils/debug.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import { toString as qrToString } from 'qrcode'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { getBridgeAccessToken } from '../../bridge/bridgeConfig.js'
|
||||
import {
|
||||
checkBridgeMinVersion,
|
||||
getBridgeDisabledReason,
|
||||
isEnvLessBridgeEnabled,
|
||||
} from '../../bridge/bridgeEnabled.js'
|
||||
import { checkEnvLessBridgeMinVersion } from '../../bridge/envLessBridgeConfig.js'
|
||||
import {
|
||||
BRIDGE_LOGIN_INSTRUCTION,
|
||||
REMOTE_CONTROL_DISCONNECTED_MSG,
|
||||
} from '../../bridge/types.js'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { ListItem } from '../../components/design-system/ListItem.js'
|
||||
import { shouldShowRemoteCallout } from '../../components/RemoteCallout.js'
|
||||
import { useRegisterOverlay } from '../../context/overlayContext.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
import { useAppState, useSetAppState } from '../../state/AppState.js'
|
||||
import type { ToolUseContext } from '../../Tool.js'
|
||||
import type {
|
||||
LocalJSXCommandContext,
|
||||
LocalJSXCommandOnDone,
|
||||
} from '../../types/command.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
|
||||
type Props = {
|
||||
onDone: LocalJSXCommandOnDone;
|
||||
name?: string;
|
||||
};
|
||||
onDone: LocalJSXCommandOnDone
|
||||
name?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* /remote-control command — manages the bidirectional bridge connection.
|
||||
@@ -35,392 +48,194 @@ type Props = {
|
||||
* Running /remote-control when already connected shows a dialog with the session
|
||||
* URL and options to disconnect or continue.
|
||||
*/
|
||||
function BridgeToggle(t0) {
|
||||
const $ = _c(10);
|
||||
const {
|
||||
onDone,
|
||||
name
|
||||
} = t0;
|
||||
const setAppState = useSetAppState();
|
||||
const replBridgeConnected = useAppState(_temp);
|
||||
const replBridgeEnabled = useAppState(_temp2);
|
||||
const replBridgeOutboundOnly = useAppState(_temp3);
|
||||
const [showDisconnectDialog, setShowDisconnectDialog] = useState(false);
|
||||
let t1;
|
||||
if ($[0] !== name || $[1] !== onDone || $[2] !== replBridgeConnected || $[3] !== replBridgeEnabled || $[4] !== replBridgeOutboundOnly || $[5] !== setAppState) {
|
||||
t1 = () => {
|
||||
if ((replBridgeConnected || replBridgeEnabled) && !replBridgeOutboundOnly) {
|
||||
setShowDisconnectDialog(true);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const error = await checkBridgePrerequisites();
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
if (error) {
|
||||
logEvent("tengu_bridge_command", {
|
||||
action: "preflight_failed" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
onDone(error, {
|
||||
display: "system"
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (shouldShowRemoteCallout()) {
|
||||
setAppState(prev => {
|
||||
if (prev.showRemoteCallout) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
showRemoteCallout: true,
|
||||
replBridgeInitialName: name
|
||||
};
|
||||
});
|
||||
onDone("", {
|
||||
display: "system"
|
||||
});
|
||||
return;
|
||||
}
|
||||
logEvent("tengu_bridge_command", {
|
||||
action: "connect" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
setAppState(prev_0 => {
|
||||
if (prev_0.replBridgeEnabled && !prev_0.replBridgeOutboundOnly) {
|
||||
return prev_0;
|
||||
}
|
||||
return {
|
||||
...prev_0,
|
||||
replBridgeEnabled: true,
|
||||
replBridgeExplicit: true,
|
||||
replBridgeOutboundOnly: false,
|
||||
replBridgeInitialName: name
|
||||
};
|
||||
});
|
||||
onDone("Remote Control connecting\u2026", {
|
||||
display: "system"
|
||||
});
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
};
|
||||
$[0] = name;
|
||||
$[1] = onDone;
|
||||
$[2] = replBridgeConnected;
|
||||
$[3] = replBridgeEnabled;
|
||||
$[4] = replBridgeOutboundOnly;
|
||||
$[5] = setAppState;
|
||||
$[6] = t1;
|
||||
} else {
|
||||
t1 = $[6];
|
||||
}
|
||||
let t2;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = [];
|
||||
$[7] = t2;
|
||||
} else {
|
||||
t2 = $[7];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
if (showDisconnectDialog) {
|
||||
let t3;
|
||||
if ($[8] !== onDone) {
|
||||
t3 = <BridgeDisconnectDialog onDone={onDone} />;
|
||||
$[8] = onDone;
|
||||
$[9] = t3;
|
||||
} else {
|
||||
t3 = $[9];
|
||||
function BridgeToggle({ onDone, name }: Props): React.ReactNode {
|
||||
const setAppState = useSetAppState()
|
||||
const replBridgeConnected = useAppState(s => s.replBridgeConnected)
|
||||
const replBridgeEnabled = useAppState(s => s.replBridgeEnabled)
|
||||
const replBridgeOutboundOnly = useAppState(s => s.replBridgeOutboundOnly)
|
||||
const [showDisconnectDialog, setShowDisconnectDialog] = useState(false)
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: bridge starts once, should not restart on state changes
|
||||
useEffect(() => {
|
||||
// If already connected or enabled in full bidirectional mode, show
|
||||
// disconnect confirmation. Outbound-only (CCR mirror) doesn't count —
|
||||
// /remote-control upgrades it to full RC instead.
|
||||
if ((replBridgeConnected || replBridgeEnabled) && !replBridgeOutboundOnly) {
|
||||
setShowDisconnectDialog(true)
|
||||
return
|
||||
}
|
||||
return t3;
|
||||
|
||||
let cancelled = false
|
||||
void (async () => {
|
||||
// Pre-flight checks before enabling (awaits GrowthBook init if disk
|
||||
// cache is stale — so Max users don't get a false "not enabled" error)
|
||||
const error = await checkBridgePrerequisites()
|
||||
if (cancelled) return
|
||||
if (error) {
|
||||
logEvent('tengu_bridge_command', {
|
||||
action:
|
||||
'preflight_failed' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
onDone(error, { display: 'system' })
|
||||
return
|
||||
}
|
||||
|
||||
// Show first-time remote dialog if not yet seen.
|
||||
// Store the name now so it's in AppState when the callout handler later
|
||||
// enables the bridge (the handler only sets replBridgeEnabled, not the name).
|
||||
if (shouldShowRemoteCallout()) {
|
||||
setAppState(prev => {
|
||||
if (prev.showRemoteCallout) return prev
|
||||
return {
|
||||
...prev,
|
||||
showRemoteCallout: true,
|
||||
replBridgeInitialName: name,
|
||||
}
|
||||
})
|
||||
onDone('', { display: 'system' })
|
||||
return
|
||||
}
|
||||
|
||||
// Enable the bridge — useReplBridge in REPL.tsx handles the rest:
|
||||
// registers environment, creates session with conversation, connects WebSocket
|
||||
logEvent('tengu_bridge_command', {
|
||||
action:
|
||||
'connect' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
setAppState(prev => {
|
||||
if (prev.replBridgeEnabled && !prev.replBridgeOutboundOnly) return prev
|
||||
return {
|
||||
...prev,
|
||||
replBridgeEnabled: true,
|
||||
replBridgeExplicit: true,
|
||||
replBridgeOutboundOnly: false,
|
||||
replBridgeInitialName: name,
|
||||
}
|
||||
})
|
||||
onDone('Remote Control connecting\u2026', {
|
||||
display: 'system',
|
||||
})
|
||||
})()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps -- run once on mount
|
||||
|
||||
if (showDisconnectDialog) {
|
||||
return <BridgeDisconnectDialog onDone={onDone} />
|
||||
}
|
||||
return null;
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog shown when /remote-control is used while the bridge is already connected.
|
||||
* Shows the session URL and lets the user disconnect or continue.
|
||||
*/
|
||||
function _temp3(s_1) {
|
||||
return s_1.replBridgeOutboundOnly;
|
||||
}
|
||||
function _temp2(s_0) {
|
||||
return s_0.replBridgeEnabled;
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.replBridgeConnected;
|
||||
}
|
||||
function BridgeDisconnectDialog(t0) {
|
||||
const $ = _c(61);
|
||||
const {
|
||||
onDone
|
||||
} = t0;
|
||||
useRegisterOverlay("bridge-disconnect-dialog", undefined);
|
||||
const setAppState = useSetAppState();
|
||||
const sessionUrl = useAppState(_temp4);
|
||||
const connectUrl = useAppState(_temp5);
|
||||
const sessionActive = useAppState(_temp6);
|
||||
const [focusIndex, setFocusIndex] = useState(2);
|
||||
const [showQR, setShowQR] = useState(false);
|
||||
const [qrText, setQrText] = useState("");
|
||||
const displayUrl = sessionActive ? sessionUrl : connectUrl;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== displayUrl || $[1] !== showQR) {
|
||||
t1 = () => {
|
||||
if (!showQR || !displayUrl) {
|
||||
setQrText("");
|
||||
return;
|
||||
}
|
||||
qrToString(displayUrl, {
|
||||
type: "utf8",
|
||||
errorCorrectionLevel: "L",
|
||||
small: true
|
||||
}).then(setQrText).catch(() => setQrText(""));
|
||||
};
|
||||
t2 = [showQR, displayUrl];
|
||||
$[0] = displayUrl;
|
||||
$[1] = showQR;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] !== onDone || $[5] !== setAppState) {
|
||||
t3 = function handleDisconnect() {
|
||||
setAppState(_temp7);
|
||||
logEvent("tengu_bridge_command", {
|
||||
action: "disconnect" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
onDone(REMOTE_CONTROL_DISCONNECTED_MSG, {
|
||||
display: "system"
|
||||
});
|
||||
};
|
||||
$[4] = onDone;
|
||||
$[5] = setAppState;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
const handleDisconnect = t3;
|
||||
let t4;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = function handleShowQR() {
|
||||
setShowQR(_temp8);
|
||||
};
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
const handleShowQR = t4;
|
||||
let t5;
|
||||
if ($[8] !== onDone) {
|
||||
t5 = function handleContinue() {
|
||||
onDone(undefined, {
|
||||
display: "skip"
|
||||
});
|
||||
};
|
||||
$[8] = onDone;
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
const handleContinue = t5;
|
||||
let t6;
|
||||
let t7;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = () => setFocusIndex(_temp9);
|
||||
t7 = () => setFocusIndex(_temp0);
|
||||
$[10] = t6;
|
||||
$[11] = t7;
|
||||
} else {
|
||||
t6 = $[10];
|
||||
t7 = $[11];
|
||||
}
|
||||
let t8;
|
||||
if ($[12] !== focusIndex || $[13] !== handleContinue || $[14] !== handleDisconnect) {
|
||||
t8 = {
|
||||
"select:next": t6,
|
||||
"select:previous": t7,
|
||||
"select:accept": () => {
|
||||
if (focusIndex === 0) {
|
||||
handleDisconnect();
|
||||
} else {
|
||||
if (focusIndex === 1) {
|
||||
handleShowQR();
|
||||
} else {
|
||||
handleContinue();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
$[12] = focusIndex;
|
||||
$[13] = handleContinue;
|
||||
$[14] = handleDisconnect;
|
||||
$[15] = t8;
|
||||
} else {
|
||||
t8 = $[15];
|
||||
}
|
||||
let t9;
|
||||
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = {
|
||||
context: "Select"
|
||||
};
|
||||
$[16] = t9;
|
||||
} else {
|
||||
t9 = $[16];
|
||||
}
|
||||
useKeybindings(t8, t9);
|
||||
let T0;
|
||||
let T1;
|
||||
let t10;
|
||||
let t11;
|
||||
let t12;
|
||||
let t13;
|
||||
let t14;
|
||||
let t15;
|
||||
let t16;
|
||||
if ($[17] !== displayUrl || $[18] !== handleContinue || $[19] !== qrText || $[20] !== showQR) {
|
||||
const qrLines = qrText ? qrText.split("\n").filter(_temp1) : [];
|
||||
T1 = Dialog;
|
||||
t14 = "Remote Control";
|
||||
t15 = handleContinue;
|
||||
t16 = true;
|
||||
T0 = Box;
|
||||
t10 = "column";
|
||||
t11 = 1;
|
||||
const t17 = displayUrl ? ` at ${displayUrl}` : "";
|
||||
if ($[30] !== t17) {
|
||||
t12 = <Text>This session is available via Remote Control{t17}.</Text>;
|
||||
$[30] = t17;
|
||||
$[31] = t12;
|
||||
} else {
|
||||
t12 = $[31];
|
||||
function BridgeDisconnectDialog({ onDone }: Props): React.ReactNode {
|
||||
useRegisterOverlay('bridge-disconnect-dialog')
|
||||
const setAppState = useSetAppState()
|
||||
const sessionUrl = useAppState(s => s.replBridgeSessionUrl)
|
||||
const connectUrl = useAppState(s => s.replBridgeConnectUrl)
|
||||
const sessionActive = useAppState(s => s.replBridgeSessionActive)
|
||||
const [focusIndex, setFocusIndex] = useState(2)
|
||||
const [showQR, setShowQR] = useState(false)
|
||||
const [qrText, setQrText] = useState('')
|
||||
|
||||
const displayUrl = sessionActive ? sessionUrl : connectUrl
|
||||
|
||||
// Generate QR code when URL changes or QR is toggled on
|
||||
useEffect(() => {
|
||||
if (!showQR || !displayUrl) {
|
||||
setQrText('')
|
||||
return
|
||||
}
|
||||
t13 = showQR && qrLines.length > 0 && <Box flexDirection="column">{qrLines.map(_temp10)}</Box>;
|
||||
$[17] = displayUrl;
|
||||
$[18] = handleContinue;
|
||||
$[19] = qrText;
|
||||
$[20] = showQR;
|
||||
$[21] = T0;
|
||||
$[22] = T1;
|
||||
$[23] = t10;
|
||||
$[24] = t11;
|
||||
$[25] = t12;
|
||||
$[26] = t13;
|
||||
$[27] = t14;
|
||||
$[28] = t15;
|
||||
$[29] = t16;
|
||||
} else {
|
||||
T0 = $[21];
|
||||
T1 = $[22];
|
||||
t10 = $[23];
|
||||
t11 = $[24];
|
||||
t12 = $[25];
|
||||
t13 = $[26];
|
||||
t14 = $[27];
|
||||
t15 = $[28];
|
||||
t16 = $[29];
|
||||
qrToString(displayUrl, {
|
||||
type: 'utf8',
|
||||
errorCorrectionLevel: 'L',
|
||||
small: true,
|
||||
})
|
||||
.then(setQrText)
|
||||
.catch(() => setQrText(''))
|
||||
}, [showQR, displayUrl])
|
||||
|
||||
function handleDisconnect(): void {
|
||||
setAppState(prev => {
|
||||
if (!prev.replBridgeEnabled) return prev
|
||||
return {
|
||||
...prev,
|
||||
replBridgeEnabled: false,
|
||||
replBridgeExplicit: false,
|
||||
replBridgeOutboundOnly: false,
|
||||
}
|
||||
})
|
||||
logEvent('tengu_bridge_command', {
|
||||
action:
|
||||
'disconnect' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
onDone(REMOTE_CONTROL_DISCONNECTED_MSG, { display: 'system' })
|
||||
}
|
||||
const t17 = focusIndex === 0;
|
||||
let t18;
|
||||
if ($[32] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t18 = <Text>Disconnect this session</Text>;
|
||||
$[32] = t18;
|
||||
} else {
|
||||
t18 = $[32];
|
||||
|
||||
function handleShowQR(): void {
|
||||
setShowQR(prev => !prev)
|
||||
}
|
||||
let t19;
|
||||
if ($[33] !== t17) {
|
||||
t19 = <ListItem isFocused={t17}>{t18}</ListItem>;
|
||||
$[33] = t17;
|
||||
$[34] = t19;
|
||||
} else {
|
||||
t19 = $[34];
|
||||
|
||||
function handleContinue(): void {
|
||||
onDone(undefined, { display: 'skip' })
|
||||
}
|
||||
const t20 = focusIndex === 1;
|
||||
const t21 = showQR ? "Hide QR code" : "Show QR code";
|
||||
let t22;
|
||||
if ($[35] !== t21) {
|
||||
t22 = <Text>{t21}</Text>;
|
||||
$[35] = t21;
|
||||
$[36] = t22;
|
||||
} else {
|
||||
t22 = $[36];
|
||||
}
|
||||
let t23;
|
||||
if ($[37] !== t20 || $[38] !== t22) {
|
||||
t23 = <ListItem isFocused={t20}>{t22}</ListItem>;
|
||||
$[37] = t20;
|
||||
$[38] = t22;
|
||||
$[39] = t23;
|
||||
} else {
|
||||
t23 = $[39];
|
||||
}
|
||||
const t24 = focusIndex === 2;
|
||||
let t25;
|
||||
if ($[40] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t25 = <Text>Continue</Text>;
|
||||
$[40] = t25;
|
||||
} else {
|
||||
t25 = $[40];
|
||||
}
|
||||
let t26;
|
||||
if ($[41] !== t24) {
|
||||
t26 = <ListItem isFocused={t24}>{t25}</ListItem>;
|
||||
$[41] = t24;
|
||||
$[42] = t26;
|
||||
} else {
|
||||
t26 = $[42];
|
||||
}
|
||||
let t27;
|
||||
if ($[43] !== t19 || $[44] !== t23 || $[45] !== t26) {
|
||||
t27 = <Box flexDirection="column">{t19}{t23}{t26}</Box>;
|
||||
$[43] = t19;
|
||||
$[44] = t23;
|
||||
$[45] = t26;
|
||||
$[46] = t27;
|
||||
} else {
|
||||
t27 = $[46];
|
||||
}
|
||||
let t28;
|
||||
if ($[47] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t28 = <Text dimColor={true}>Enter to select · Esc to continue</Text>;
|
||||
$[47] = t28;
|
||||
} else {
|
||||
t28 = $[47];
|
||||
}
|
||||
let t29;
|
||||
if ($[48] !== T0 || $[49] !== t10 || $[50] !== t11 || $[51] !== t12 || $[52] !== t13 || $[53] !== t27) {
|
||||
t29 = <T0 flexDirection={t10} gap={t11}>{t12}{t13}{t27}{t28}</T0>;
|
||||
$[48] = T0;
|
||||
$[49] = t10;
|
||||
$[50] = t11;
|
||||
$[51] = t12;
|
||||
$[52] = t13;
|
||||
$[53] = t27;
|
||||
$[54] = t29;
|
||||
} else {
|
||||
t29 = $[54];
|
||||
}
|
||||
let t30;
|
||||
if ($[55] !== T1 || $[56] !== t14 || $[57] !== t15 || $[58] !== t16 || $[59] !== t29) {
|
||||
t30 = <T1 title={t14} onCancel={t15} hideInputGuide={t16}>{t29}</T1>;
|
||||
$[55] = T1;
|
||||
$[56] = t14;
|
||||
$[57] = t15;
|
||||
$[58] = t16;
|
||||
$[59] = t29;
|
||||
$[60] = t30;
|
||||
} else {
|
||||
t30 = $[60];
|
||||
}
|
||||
return t30;
|
||||
|
||||
const ITEM_COUNT = 3
|
||||
|
||||
useKeybindings(
|
||||
{
|
||||
'select:next': () => setFocusIndex(i => (i + 1) % ITEM_COUNT),
|
||||
'select:previous': () =>
|
||||
setFocusIndex(i => (i - 1 + ITEM_COUNT) % ITEM_COUNT),
|
||||
'select:accept': () => {
|
||||
if (focusIndex === 0) {
|
||||
handleDisconnect()
|
||||
} else if (focusIndex === 1) {
|
||||
handleShowQR()
|
||||
} else {
|
||||
handleContinue()
|
||||
}
|
||||
},
|
||||
},
|
||||
{ context: 'Select' },
|
||||
)
|
||||
|
||||
const qrLines = qrText ? qrText.split('\n').filter(l => l.length > 0) : []
|
||||
|
||||
return (
|
||||
<Dialog title="Remote Control" onCancel={handleContinue} hideInputGuide>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text>
|
||||
This session is available via Remote Control
|
||||
{displayUrl ? ` at ${displayUrl}` : ''}.
|
||||
</Text>
|
||||
{showQR && qrLines.length > 0 && (
|
||||
<Box flexDirection="column">
|
||||
{qrLines.map((line, i) => (
|
||||
<Text key={i}>{line}</Text>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
<Box flexDirection="column">
|
||||
<ListItem isFocused={focusIndex === 0}>
|
||||
<Text>Disconnect this session</Text>
|
||||
</ListItem>
|
||||
<ListItem isFocused={focusIndex === 1}>
|
||||
<Text>{showQR ? 'Hide QR code' : 'Show QR code'}</Text>
|
||||
</ListItem>
|
||||
<ListItem isFocused={focusIndex === 2}>
|
||||
<Text>Continue</Text>
|
||||
</ListItem>
|
||||
</Box>
|
||||
<Text dimColor>Enter to select · Esc to continue</Text>
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -429,80 +244,52 @@ function BridgeDisconnectDialog(t0) {
|
||||
* cache is stale, so a user who just became entitled (e.g. upgraded to Max,
|
||||
* or the flag just launched) gets an accurate result on the first try.
|
||||
*/
|
||||
function _temp10(line, i_1) {
|
||||
return <Text key={i_1}>{line}</Text>;
|
||||
}
|
||||
function _temp1(l) {
|
||||
return l.length > 0;
|
||||
}
|
||||
function _temp0(i_0) {
|
||||
return (i_0 - 1 + 3) % 3;
|
||||
}
|
||||
function _temp9(i) {
|
||||
return (i + 1) % 3;
|
||||
}
|
||||
function _temp8(prev_0) {
|
||||
return !prev_0;
|
||||
}
|
||||
function _temp7(prev) {
|
||||
if (!prev.replBridgeEnabled) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
replBridgeEnabled: false,
|
||||
replBridgeExplicit: false,
|
||||
replBridgeOutboundOnly: false
|
||||
};
|
||||
}
|
||||
function _temp6(s_1) {
|
||||
return s_1.replBridgeSessionActive;
|
||||
}
|
||||
function _temp5(s_0) {
|
||||
return s_0.replBridgeConnectUrl;
|
||||
}
|
||||
function _temp4(s) {
|
||||
return s.replBridgeSessionUrl;
|
||||
}
|
||||
async function checkBridgePrerequisites(): Promise<string | null> {
|
||||
// Check organization policy — remote control may be disabled
|
||||
const {
|
||||
waitForPolicyLimitsToLoad,
|
||||
isPolicyAllowed
|
||||
} = await import('../../services/policyLimits/index.js');
|
||||
await waitForPolicyLimitsToLoad();
|
||||
const { waitForPolicyLimitsToLoad, isPolicyAllowed } = await import(
|
||||
'../../services/policyLimits/index.js'
|
||||
)
|
||||
await waitForPolicyLimitsToLoad()
|
||||
if (!isPolicyAllowed('allow_remote_control')) {
|
||||
return "Remote Control is disabled by your organization's policy.";
|
||||
return "Remote Control is disabled by your organization's policy."
|
||||
}
|
||||
const disabledReason = await getBridgeDisabledReason();
|
||||
|
||||
const disabledReason = await getBridgeDisabledReason()
|
||||
if (disabledReason) {
|
||||
return disabledReason;
|
||||
return disabledReason
|
||||
}
|
||||
|
||||
// Mirror the v1/v2 branching logic in initReplBridge: env-less (v2) is used
|
||||
// only when the flag is on AND the session is not perpetual. In assistant
|
||||
// mode (KAIROS) useReplBridge sets perpetual=true, which forces
|
||||
// initReplBridge onto the v1 path — so the prerequisite check must match.
|
||||
let useV2 = isEnvLessBridgeEnabled();
|
||||
let useV2 = isEnvLessBridgeEnabled()
|
||||
if (feature('KAIROS') && useV2) {
|
||||
const {
|
||||
isAssistantMode
|
||||
} = await import('../../assistant/index.js');
|
||||
const { isAssistantMode } = await import('../../assistant/index.js')
|
||||
if (isAssistantMode()) {
|
||||
useV2 = false;
|
||||
useV2 = false
|
||||
}
|
||||
}
|
||||
const versionError = useV2 ? await checkEnvLessBridgeMinVersion() : checkBridgeMinVersion();
|
||||
const versionError = useV2
|
||||
? await checkEnvLessBridgeMinVersion()
|
||||
: checkBridgeMinVersion()
|
||||
if (versionError) {
|
||||
return versionError;
|
||||
return versionError
|
||||
}
|
||||
|
||||
if (!getBridgeAccessToken()) {
|
||||
return BRIDGE_LOGIN_INSTRUCTION;
|
||||
return BRIDGE_LOGIN_INSTRUCTION
|
||||
}
|
||||
logForDebugging('[bridge] Prerequisites passed, enabling bridge');
|
||||
return null;
|
||||
|
||||
logForDebugging('[bridge] Prerequisites passed, enabling bridge')
|
||||
return null
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, _context: ToolUseContext & LocalJSXCommandContext, args: string): Promise<React.ReactNode> {
|
||||
const name = args.trim() || undefined;
|
||||
return <BridgeToggle onDone={onDone} name={name} />;
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
_context: ToolUseContext & LocalJSXCommandContext,
|
||||
args: string,
|
||||
): Promise<React.ReactNode> {
|
||||
const name = args.trim() || undefined
|
||||
return <BridgeToggle onDone={onDone} name={name} />
|
||||
}
|
||||
|
||||
@@ -1,183 +1,151 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useInterval } from 'usehooks-ts';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import { Markdown } from '../../components/Markdown.js';
|
||||
import { SpinnerGlyph } from '../../components/Spinner/SpinnerGlyph.js';
|
||||
import { DOWN_ARROW, UP_ARROW } from '../../constants/figures.js';
|
||||
import { getSystemPrompt } from '../../constants/prompts.js';
|
||||
import { useModalOrTerminalSize } from '../../context/modalContext.js';
|
||||
import { getSystemContext, getUserContext } from '../../context.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import ScrollBox, { type ScrollBoxHandle } from '../../ink/components/ScrollBox.js';
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import type { Message } from '../../types/message.js';
|
||||
import { createAbortController } from '../../utils/abortController.js';
|
||||
import { saveGlobalConfig } from '../../utils/config.js';
|
||||
import { errorMessage } from '../../utils/errors.js';
|
||||
import { type CacheSafeParams, getLastCacheSafeParams } from '../../utils/forkedAgent.js';
|
||||
import { getMessagesAfterCompactBoundary } from '../../utils/messages.js';
|
||||
import type { ProcessUserInputContext } from '../../utils/processUserInput/processUserInput.js';
|
||||
import { runSideQuestion } from '../../utils/sideQuestion.js';
|
||||
import { asSystemPrompt } from '../../utils/systemPromptType.js';
|
||||
import * as React from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useInterval } from 'usehooks-ts'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import { Markdown } from '../../components/Markdown.js'
|
||||
import { SpinnerGlyph } from '../../components/Spinner/SpinnerGlyph.js'
|
||||
import { DOWN_ARROW, UP_ARROW } from '../../constants/figures.js'
|
||||
import { getSystemPrompt } from '../../constants/prompts.js'
|
||||
import { useModalOrTerminalSize } from '../../context/modalContext.js'
|
||||
import { getSystemContext, getUserContext } from '../../context.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import ScrollBox, {
|
||||
type ScrollBoxHandle,
|
||||
} from '../../ink/components/ScrollBox.js'
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import type { Message } from '../../types/message.js'
|
||||
import { createAbortController } from '../../utils/abortController.js'
|
||||
import { saveGlobalConfig } from '../../utils/config.js'
|
||||
import { errorMessage } from '../../utils/errors.js'
|
||||
import {
|
||||
type CacheSafeParams,
|
||||
getLastCacheSafeParams,
|
||||
} from '../../utils/forkedAgent.js'
|
||||
import { getMessagesAfterCompactBoundary } from '../../utils/messages.js'
|
||||
import type { ProcessUserInputContext } from '../../utils/processUserInput/processUserInput.js'
|
||||
import { runSideQuestion } from '../../utils/sideQuestion.js'
|
||||
import { asSystemPrompt } from '../../utils/systemPromptType.js'
|
||||
|
||||
type BtwComponentProps = {
|
||||
question: string;
|
||||
context: ProcessUserInputContext;
|
||||
onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
};
|
||||
const CHROME_ROWS = 5;
|
||||
const OUTER_CHROME_ROWS = 6;
|
||||
const SCROLL_LINES = 3;
|
||||
function BtwSideQuestion(t0) {
|
||||
const $ = _c(25);
|
||||
const {
|
||||
question,
|
||||
context,
|
||||
onDone
|
||||
} = t0;
|
||||
const [response, setResponse] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [frame, setFrame] = useState(0);
|
||||
const scrollRef = useRef(null);
|
||||
const {
|
||||
rows
|
||||
} = useModalOrTerminalSize(useTerminalSize());
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = () => setFrame(_temp);
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
question: string
|
||||
context: ProcessUserInputContext
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
}
|
||||
|
||||
const CHROME_ROWS = 5
|
||||
const OUTER_CHROME_ROWS = 6
|
||||
const SCROLL_LINES = 3
|
||||
|
||||
function BtwSideQuestion({
|
||||
question,
|
||||
context,
|
||||
onDone,
|
||||
}: BtwComponentProps): React.ReactNode {
|
||||
const [response, setResponse] = useState<string | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [frame, setFrame] = useState(0)
|
||||
const scrollRef = useRef<ScrollBoxHandle>(null)
|
||||
const { rows } = useModalOrTerminalSize(useTerminalSize())
|
||||
|
||||
// Animate spinner while loading
|
||||
useInterval(() => setFrame(f => f + 1), response || error ? null : 80)
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent): void {
|
||||
if (
|
||||
e.key === 'escape' ||
|
||||
e.key === 'return' ||
|
||||
e.key === ' ' ||
|
||||
(e.ctrl && (e.key === 'c' || e.key === 'd'))
|
||||
) {
|
||||
e.preventDefault()
|
||||
onDone(undefined, { display: 'skip' })
|
||||
return
|
||||
}
|
||||
if (e.key === 'up' || (e.ctrl && e.key === 'p')) {
|
||||
e.preventDefault()
|
||||
scrollRef.current?.scrollBy(-SCROLL_LINES)
|
||||
}
|
||||
if (e.key === 'down' || (e.ctrl && e.key === 'n')) {
|
||||
e.preventDefault()
|
||||
scrollRef.current?.scrollBy(SCROLL_LINES)
|
||||
}
|
||||
}
|
||||
useInterval(t1, response || error ? null : 80);
|
||||
let t2;
|
||||
if ($[1] !== onDone) {
|
||||
t2 = function handleKeyDown(e) {
|
||||
if (e.key === "escape" || e.key === "return" || e.key === " " || e.ctrl && (e.key === "c" || e.key === "d")) {
|
||||
e.preventDefault();
|
||||
onDone(undefined, {
|
||||
display: "skip"
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (e.key === "up" || e.ctrl && e.key === "p") {
|
||||
e.preventDefault();
|
||||
scrollRef.current?.scrollBy(-SCROLL_LINES);
|
||||
}
|
||||
if (e.key === "down" || e.ctrl && e.key === "n") {
|
||||
e.preventDefault();
|
||||
scrollRef.current?.scrollBy(SCROLL_LINES);
|
||||
}
|
||||
};
|
||||
$[1] = onDone;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
const handleKeyDown = t2;
|
||||
let t3;
|
||||
let t4;
|
||||
if ($[3] !== context || $[4] !== question) {
|
||||
t3 = () => {
|
||||
const abortController = createAbortController();
|
||||
const fetchResponse = async function fetchResponse() {
|
||||
;
|
||||
try {
|
||||
const cacheSafeParams = await buildCacheSafeParams(context);
|
||||
const result = await runSideQuestion({
|
||||
question,
|
||||
cacheSafeParams
|
||||
});
|
||||
if (!abortController.signal.aborted) {
|
||||
if (result.response) {
|
||||
setResponse(result.response);
|
||||
} else {
|
||||
setError("No response received");
|
||||
}
|
||||
}
|
||||
} catch (t5) {
|
||||
const err = t5;
|
||||
if (!abortController.signal.aborted) {
|
||||
setError(errorMessage(err) || "Failed to get response");
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = createAbortController()
|
||||
|
||||
async function fetchResponse(): Promise<void> {
|
||||
try {
|
||||
const cacheSafeParams = await buildCacheSafeParams(context)
|
||||
const result = await runSideQuestion({ question, cacheSafeParams })
|
||||
|
||||
if (!abortController.signal.aborted) {
|
||||
if (result.response) {
|
||||
setResponse(result.response)
|
||||
} else {
|
||||
setError('No response received')
|
||||
}
|
||||
}
|
||||
};
|
||||
fetchResponse();
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
};
|
||||
t4 = [question, context];
|
||||
$[3] = context;
|
||||
$[4] = question;
|
||||
$[5] = t3;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
t4 = $[6];
|
||||
}
|
||||
useEffect(t3, t4);
|
||||
const maxContentHeight = Math.max(5, rows - CHROME_ROWS - OUTER_CHROME_ROWS);
|
||||
let t5;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Text color="warning" bold={true}>/btw{" "}</Text>;
|
||||
$[7] = t5;
|
||||
} else {
|
||||
t5 = $[7];
|
||||
}
|
||||
let t6;
|
||||
if ($[8] !== question) {
|
||||
t6 = <Box>{t5}<Text dimColor={true}>{question}</Text></Box>;
|
||||
$[8] = question;
|
||||
$[9] = t6;
|
||||
} else {
|
||||
t6 = $[9];
|
||||
}
|
||||
let t7;
|
||||
if ($[10] !== error || $[11] !== frame || $[12] !== response) {
|
||||
t7 = <ScrollBox ref={scrollRef} flexDirection="column" flexGrow={1}>{error ? <Text color="error">{error}</Text> : response ? <Markdown>{response}</Markdown> : <Box><SpinnerGlyph frame={frame} messageColor="warning" /><Text color="warning">Answering...</Text></Box>}</ScrollBox>;
|
||||
$[10] = error;
|
||||
$[11] = frame;
|
||||
$[12] = response;
|
||||
$[13] = t7;
|
||||
} else {
|
||||
t7 = $[13];
|
||||
}
|
||||
let t8;
|
||||
if ($[14] !== maxContentHeight || $[15] !== t7) {
|
||||
t8 = <Box marginTop={1} marginLeft={2} maxHeight={maxContentHeight}>{t7}</Box>;
|
||||
$[14] = maxContentHeight;
|
||||
$[15] = t7;
|
||||
$[16] = t8;
|
||||
} else {
|
||||
t8 = $[16];
|
||||
}
|
||||
let t9;
|
||||
if ($[17] !== error || $[18] !== response) {
|
||||
t9 = (response || error) && <Box marginTop={1}><Text dimColor={true}>{UP_ARROW}/{DOWN_ARROW} to scroll · Space, Enter, or Escape to dismiss</Text></Box>;
|
||||
$[17] = error;
|
||||
$[18] = response;
|
||||
$[19] = t9;
|
||||
} else {
|
||||
t9 = $[19];
|
||||
}
|
||||
let t10;
|
||||
if ($[20] !== handleKeyDown || $[21] !== t6 || $[22] !== t8 || $[23] !== t9) {
|
||||
t10 = <Box flexDirection="column" paddingLeft={2} marginTop={1} tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t6}{t8}{t9}</Box>;
|
||||
$[20] = handleKeyDown;
|
||||
$[21] = t6;
|
||||
$[22] = t8;
|
||||
$[23] = t9;
|
||||
$[24] = t10;
|
||||
} else {
|
||||
t10 = $[24];
|
||||
}
|
||||
return t10;
|
||||
} catch (err) {
|
||||
if (!abortController.signal.aborted) {
|
||||
setError(errorMessage(err) || 'Failed to get response')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void fetchResponse()
|
||||
|
||||
return () => {
|
||||
abortController.abort()
|
||||
}
|
||||
}, [question, context])
|
||||
|
||||
const maxContentHeight = Math.max(5, rows - CHROME_ROWS - OUTER_CHROME_ROWS)
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
paddingLeft={2}
|
||||
marginTop={1}
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Box>
|
||||
<Text color="warning" bold>
|
||||
/btw{' '}
|
||||
</Text>
|
||||
<Text dimColor>{question}</Text>
|
||||
</Box>
|
||||
<Box marginTop={1} marginLeft={2} maxHeight={maxContentHeight}>
|
||||
<ScrollBox ref={scrollRef} flexDirection="column" flexGrow={1}>
|
||||
{error ? (
|
||||
<Text color="error">{error}</Text>
|
||||
) : response ? (
|
||||
<Markdown>{response}</Markdown>
|
||||
) : (
|
||||
<Box>
|
||||
<SpinnerGlyph frame={frame} messageColor="warning" />
|
||||
<Text color="warning">Answering...</Text>
|
||||
</Box>
|
||||
)}
|
||||
</ScrollBox>
|
||||
</Box>
|
||||
{(response || error) && (
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
{UP_ARROW}/{DOWN_ARROW} to scroll · Space, Enter, or Escape to
|
||||
dismiss
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,48 +163,67 @@ function BtwSideQuestion(t0) {
|
||||
* applied buildEffectiveSystemPrompt extras (--agent, --system-prompt,
|
||||
* --append-system-prompt, coordinator mode).
|
||||
*/
|
||||
function _temp(f) {
|
||||
return f + 1;
|
||||
}
|
||||
function stripInProgressAssistantMessage(messages: Message[]): Message[] {
|
||||
const last = messages.at(-1);
|
||||
const last = messages.at(-1)
|
||||
if (last?.type === 'assistant' && last.message.stop_reason === null) {
|
||||
return messages.slice(0, -1);
|
||||
return messages.slice(0, -1)
|
||||
}
|
||||
return messages;
|
||||
return messages
|
||||
}
|
||||
async function buildCacheSafeParams(context: ProcessUserInputContext): Promise<CacheSafeParams> {
|
||||
const forkContextMessages = getMessagesAfterCompactBoundary(stripInProgressAssistantMessage(context.messages));
|
||||
const saved = getLastCacheSafeParams();
|
||||
|
||||
async function buildCacheSafeParams(
|
||||
context: ProcessUserInputContext,
|
||||
): Promise<CacheSafeParams> {
|
||||
const forkContextMessages = getMessagesAfterCompactBoundary(
|
||||
stripInProgressAssistantMessage(context.messages),
|
||||
)
|
||||
const saved = getLastCacheSafeParams()
|
||||
if (saved) {
|
||||
return {
|
||||
systemPrompt: saved.systemPrompt,
|
||||
userContext: saved.userContext,
|
||||
systemContext: saved.systemContext,
|
||||
toolUseContext: context,
|
||||
forkContextMessages
|
||||
};
|
||||
forkContextMessages,
|
||||
}
|
||||
}
|
||||
const [rawSystemPrompt, userContext, systemContext] = await Promise.all([getSystemPrompt(context.options.tools, context.options.mainLoopModel, [], context.options.mcpClients), getUserContext(), getSystemContext()]);
|
||||
const [rawSystemPrompt, userContext, systemContext] = await Promise.all([
|
||||
getSystemPrompt(
|
||||
context.options.tools,
|
||||
context.options.mainLoopModel,
|
||||
[],
|
||||
context.options.mcpClients,
|
||||
),
|
||||
getUserContext(),
|
||||
getSystemContext(),
|
||||
])
|
||||
return {
|
||||
systemPrompt: asSystemPrompt(rawSystemPrompt),
|
||||
userContext,
|
||||
systemContext,
|
||||
toolUseContext: context,
|
||||
forkContextMessages
|
||||
};
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: ProcessUserInputContext, args: string): Promise<React.ReactNode> {
|
||||
const question = args?.trim();
|
||||
if (!question) {
|
||||
onDone('Usage: /btw <your question>', {
|
||||
display: 'system'
|
||||
});
|
||||
return null;
|
||||
forkContextMessages,
|
||||
}
|
||||
}
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: ProcessUserInputContext,
|
||||
args: string,
|
||||
): Promise<React.ReactNode> {
|
||||
const question = args?.trim()
|
||||
|
||||
if (!question) {
|
||||
onDone('Usage: /btw <your question>', { display: 'system' })
|
||||
return null
|
||||
}
|
||||
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
btwUseCount: current.btwUseCount + 1
|
||||
}));
|
||||
return <BtwSideQuestion question={question} context={context} onDone={onDone} />;
|
||||
btwUseCount: current.btwUseCount + 1,
|
||||
}))
|
||||
|
||||
return (
|
||||
<BtwSideQuestion question={question} context={context} onDone={onDone} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,284 +1,240 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useState } from 'react';
|
||||
import { type OptionWithDescription, Select } from '../../components/CustomSelect/select.js';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useAppState } from '../../state/AppState.js';
|
||||
import { isClaudeAISubscriber } from '../../utils/auth.js';
|
||||
import { openBrowser } from '../../utils/browser.js';
|
||||
import { CLAUDE_IN_CHROME_MCP_SERVER_NAME, openInChrome } from '../../utils/claudeInChrome/common.js';
|
||||
import { isChromeExtensionInstalled } from '../../utils/claudeInChrome/setup.js';
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
|
||||
import { env } from '../../utils/env.js';
|
||||
import { isRunningOnHomespace } from '../../utils/envUtils.js';
|
||||
const CHROME_EXTENSION_URL = 'https://claude.ai/chrome';
|
||||
const CHROME_PERMISSIONS_URL = 'https://clau.de/chrome/permissions';
|
||||
const CHROME_RECONNECT_URL = 'https://clau.de/chrome/reconnect';
|
||||
type MenuAction = 'install-extension' | 'reconnect' | 'manage-permissions' | 'toggle-default';
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
type OptionWithDescription,
|
||||
Select,
|
||||
} from '../../components/CustomSelect/select.js'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useAppState } from '../../state/AppState.js'
|
||||
import { isClaudeAISubscriber } from '../../utils/auth.js'
|
||||
import { openBrowser } from '../../utils/browser.js'
|
||||
import {
|
||||
CLAUDE_IN_CHROME_MCP_SERVER_NAME,
|
||||
openInChrome,
|
||||
} from '../../utils/claudeInChrome/common.js'
|
||||
import { isChromeExtensionInstalled } from '../../utils/claudeInChrome/setup.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||
import { env } from '../../utils/env.js'
|
||||
import { isRunningOnHomespace } from '../../utils/envUtils.js'
|
||||
|
||||
const CHROME_EXTENSION_URL = 'https://claude.ai/chrome'
|
||||
const CHROME_PERMISSIONS_URL = 'https://clau.de/chrome/permissions'
|
||||
const CHROME_RECONNECT_URL = 'https://clau.de/chrome/reconnect'
|
||||
|
||||
type MenuAction =
|
||||
| 'install-extension'
|
||||
| 'reconnect'
|
||||
| 'manage-permissions'
|
||||
| 'toggle-default'
|
||||
|
||||
type Props = {
|
||||
onDone: (result?: string) => void;
|
||||
isExtensionInstalled: boolean;
|
||||
configEnabled: boolean | undefined;
|
||||
isClaudeAISubscriber: boolean;
|
||||
isWSL: boolean;
|
||||
};
|
||||
function ClaudeInChromeMenu(t0) {
|
||||
const $ = _c(41);
|
||||
const {
|
||||
onDone,
|
||||
isExtensionInstalled: installed,
|
||||
configEnabled,
|
||||
isClaudeAISubscriber,
|
||||
isWSL
|
||||
} = t0;
|
||||
const mcpClients = useAppState(_temp);
|
||||
const [selectKey, setSelectKey] = useState(0);
|
||||
const [enabledByDefault, setEnabledByDefault] = useState(configEnabled ?? false);
|
||||
const [showInstallHint, setShowInstallHint] = useState(false);
|
||||
const [isExtensionInstalled, setIsExtensionInstalled] = useState(installed);
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = false && isRunningOnHomespace();
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
onDone: (result?: string) => void
|
||||
isExtensionInstalled: boolean
|
||||
configEnabled: boolean | undefined
|
||||
isClaudeAISubscriber: boolean
|
||||
isWSL: boolean
|
||||
}
|
||||
|
||||
function ClaudeInChromeMenu({
|
||||
onDone,
|
||||
isExtensionInstalled: installed,
|
||||
configEnabled,
|
||||
isClaudeAISubscriber,
|
||||
isWSL,
|
||||
}: Props): React.ReactNode {
|
||||
const mcpClients = useAppState(s => s.mcp.clients)
|
||||
const [selectKey, setSelectKey] = useState(0)
|
||||
const [enabledByDefault, setEnabledByDefault] = useState(
|
||||
configEnabled ?? false,
|
||||
)
|
||||
const [showInstallHint, setShowInstallHint] = useState(false)
|
||||
const [isExtensionInstalled, setIsExtensionInstalled] = useState(installed)
|
||||
|
||||
const isHomespace = process.env.USER_TYPE === 'ant' && isRunningOnHomespace()
|
||||
|
||||
const chromeClient = mcpClients.find(
|
||||
c => c.name === CLAUDE_IN_CHROME_MCP_SERVER_NAME,
|
||||
)
|
||||
const isConnected = chromeClient?.type === 'connected'
|
||||
|
||||
function openUrl(url: string): void {
|
||||
if (isHomespace) {
|
||||
void openBrowser(url)
|
||||
} else {
|
||||
void openInChrome(url)
|
||||
}
|
||||
}
|
||||
const isHomespace = t1;
|
||||
let t2;
|
||||
if ($[1] !== mcpClients) {
|
||||
t2 = mcpClients.find(_temp2);
|
||||
$[1] = mcpClients;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
const chromeClient = t2;
|
||||
const isConnected = chromeClient?.type === "connected";
|
||||
let t3;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = function openUrl(url) {
|
||||
if (isHomespace) {
|
||||
openBrowser(url);
|
||||
} else {
|
||||
openInChrome(url);
|
||||
|
||||
function handleAction(action: MenuAction): void {
|
||||
switch (action) {
|
||||
case 'install-extension':
|
||||
setSelectKey(k => k + 1)
|
||||
setShowInstallHint(true)
|
||||
openUrl(CHROME_EXTENSION_URL)
|
||||
break
|
||||
case 'reconnect':
|
||||
setSelectKey(k => k + 1)
|
||||
void isChromeExtensionInstalled().then(installed => {
|
||||
setIsExtensionInstalled(installed)
|
||||
if (installed) {
|
||||
setShowInstallHint(false)
|
||||
}
|
||||
})
|
||||
openUrl(CHROME_RECONNECT_URL)
|
||||
break
|
||||
case 'manage-permissions':
|
||||
setSelectKey(k => k + 1)
|
||||
openUrl(CHROME_PERMISSIONS_URL)
|
||||
break
|
||||
case 'toggle-default': {
|
||||
const newValue = !enabledByDefault
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
claudeInChromeDefaultEnabled: newValue,
|
||||
}))
|
||||
setEnabledByDefault(newValue)
|
||||
break
|
||||
}
|
||||
};
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
const openUrl = t3;
|
||||
let t4;
|
||||
if ($[4] !== enabledByDefault) {
|
||||
t4 = function handleAction(action) {
|
||||
bb22: switch (action) {
|
||||
case "install-extension":
|
||||
{
|
||||
setSelectKey(_temp3);
|
||||
setShowInstallHint(true);
|
||||
openUrl(CHROME_EXTENSION_URL);
|
||||
break bb22;
|
||||
}
|
||||
case "reconnect":
|
||||
{
|
||||
setSelectKey(_temp4);
|
||||
isChromeExtensionInstalled().then(installed_0 => {
|
||||
setIsExtensionInstalled(installed_0);
|
||||
if (installed_0) {
|
||||
setShowInstallHint(false);
|
||||
}
|
||||
});
|
||||
openUrl(CHROME_RECONNECT_URL);
|
||||
break bb22;
|
||||
}
|
||||
case "manage-permissions":
|
||||
{
|
||||
setSelectKey(_temp5);
|
||||
openUrl(CHROME_PERMISSIONS_URL);
|
||||
break bb22;
|
||||
}
|
||||
case "toggle-default":
|
||||
{
|
||||
const newValue = !enabledByDefault;
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
claudeInChromeDefaultEnabled: newValue
|
||||
}));
|
||||
setEnabledByDefault(newValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
$[4] = enabledByDefault;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
const handleAction = t4;
|
||||
let options;
|
||||
if ($[6] !== enabledByDefault || $[7] !== isExtensionInstalled) {
|
||||
options = [];
|
||||
const requiresExtensionSuffix = isExtensionInstalled ? "" : " (requires extension)";
|
||||
if (!isExtensionInstalled && !isHomespace) {
|
||||
let t5;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = {
|
||||
label: "Install Chrome extension",
|
||||
value: "install-extension"
|
||||
};
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
options.push(t5);
|
||||
}
|
||||
let t5;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Text>Manage permissions</Text>;
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
}
|
||||
let t6;
|
||||
if ($[11] !== requiresExtensionSuffix) {
|
||||
t6 = {
|
||||
label: <>{t5}<Text dimColor={true}>{requiresExtensionSuffix}</Text></>,
|
||||
value: "manage-permissions"
|
||||
};
|
||||
$[11] = requiresExtensionSuffix;
|
||||
$[12] = t6;
|
||||
} else {
|
||||
t6 = $[12];
|
||||
}
|
||||
let t7;
|
||||
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Text>Reconnect extension</Text>;
|
||||
$[13] = t7;
|
||||
} else {
|
||||
t7 = $[13];
|
||||
}
|
||||
let t8;
|
||||
if ($[14] !== requiresExtensionSuffix) {
|
||||
t8 = {
|
||||
label: <>{t7}<Text dimColor={true}>{requiresExtensionSuffix}</Text></>,
|
||||
value: "reconnect"
|
||||
};
|
||||
$[14] = requiresExtensionSuffix;
|
||||
$[15] = t8;
|
||||
} else {
|
||||
t8 = $[15];
|
||||
}
|
||||
const t9 = `Enabled by default: ${enabledByDefault ? "Yes" : "No"}`;
|
||||
let t10;
|
||||
if ($[16] !== t9) {
|
||||
t10 = {
|
||||
label: t9,
|
||||
value: "toggle-default"
|
||||
};
|
||||
$[16] = t9;
|
||||
$[17] = t10;
|
||||
} else {
|
||||
t10 = $[17];
|
||||
}
|
||||
options.push(t6, t8, t10);
|
||||
$[6] = enabledByDefault;
|
||||
$[7] = isExtensionInstalled;
|
||||
$[8] = options;
|
||||
} else {
|
||||
options = $[8];
|
||||
}
|
||||
const isDisabled = isWSL;
|
||||
let t5;
|
||||
if ($[18] !== onDone) {
|
||||
t5 = () => onDone();
|
||||
$[18] = onDone;
|
||||
$[19] = t5;
|
||||
} else {
|
||||
t5 = $[19];
|
||||
|
||||
const options: OptionWithDescription<MenuAction>[] = []
|
||||
const requiresExtensionSuffix = isExtensionInstalled
|
||||
? ''
|
||||
: ' (requires extension)'
|
||||
|
||||
if (!isExtensionInstalled && !isHomespace) {
|
||||
options.push({
|
||||
label: 'Install Chrome extension',
|
||||
value: 'install-extension',
|
||||
})
|
||||
}
|
||||
let t6;
|
||||
if ($[20] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = <Text>Claude in Chrome works with the Chrome extension to let you control your browser directly from Claude Code. Navigate websites, fill forms, capture screenshots, record GIFs, and debug with console logs and network requests.</Text>;
|
||||
$[20] = t6;
|
||||
} else {
|
||||
t6 = $[20];
|
||||
}
|
||||
let t7;
|
||||
if ($[21] !== isWSL) {
|
||||
t7 = isWSL && <Text color="error">Claude in Chrome is not supported in WSL at this time.</Text>;
|
||||
$[21] = isWSL;
|
||||
$[22] = t7;
|
||||
} else {
|
||||
t7 = $[22];
|
||||
}
|
||||
let t8;
|
||||
if ($[23] !== isClaudeAISubscriber) {
|
||||
t8 = false;
|
||||
$[23] = isClaudeAISubscriber;
|
||||
$[24] = t8;
|
||||
} else {
|
||||
t8 = $[24];
|
||||
}
|
||||
let t9;
|
||||
if ($[25] !== handleAction || $[26] !== isConnected || $[27] !== isDisabled || $[28] !== isExtensionInstalled || $[29] !== options || $[30] !== selectKey || $[31] !== showInstallHint) {
|
||||
t9 = !isDisabled && <>{!isHomespace && <Box flexDirection="column"><Text>Status:{" "}{isConnected ? <Text color="success">Enabled</Text> : <Text color="inactive">Disabled</Text>}</Text><Text>Extension:{" "}{isExtensionInstalled ? <Text color="success">Installed</Text> : <Text color="warning">Not detected</Text>}</Text></Box>}<Select key={selectKey} options={options} onChange={handleAction} hideIndexes={true} />{showInstallHint && <Text color="warning">Once installed, select {"\"Reconnect extension\""} to connect.</Text>}<Text><Text dimColor={true}>Usage: </Text><Text>claude --chrome</Text><Text dimColor={true}> or </Text><Text>claude --no-chrome</Text></Text><Text dimColor={true}>Site-level permissions are inherited from the Chrome extension. Manage permissions in the Chrome extension settings to control which sites Claude can browse, click, and type on.</Text></>;
|
||||
$[25] = handleAction;
|
||||
$[26] = isConnected;
|
||||
$[27] = isDisabled;
|
||||
$[28] = isExtensionInstalled;
|
||||
$[29] = options;
|
||||
$[30] = selectKey;
|
||||
$[31] = showInstallHint;
|
||||
$[32] = t9;
|
||||
} else {
|
||||
t9 = $[32];
|
||||
}
|
||||
let t10;
|
||||
if ($[33] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t10 = <Text dimColor={true}>Learn more: https://code.claude.com/docs/en/chrome</Text>;
|
||||
$[33] = t10;
|
||||
} else {
|
||||
t10 = $[33];
|
||||
}
|
||||
let t11;
|
||||
if ($[34] !== t7 || $[35] !== t8 || $[36] !== t9) {
|
||||
t11 = <Box flexDirection="column" gap={1}>{t6}{t7}{t8}{t9}{t10}</Box>;
|
||||
$[34] = t7;
|
||||
$[35] = t8;
|
||||
$[36] = t9;
|
||||
$[37] = t11;
|
||||
} else {
|
||||
t11 = $[37];
|
||||
}
|
||||
let t12;
|
||||
if ($[38] !== t11 || $[39] !== t5) {
|
||||
t12 = <Dialog title="Claude in Chrome (Beta)" onCancel={t5} color="chromeYellow">{t11}</Dialog>;
|
||||
$[38] = t11;
|
||||
$[39] = t5;
|
||||
$[40] = t12;
|
||||
} else {
|
||||
t12 = $[40];
|
||||
}
|
||||
return t12;
|
||||
|
||||
options.push(
|
||||
{
|
||||
label: (
|
||||
<>
|
||||
<Text>Manage permissions</Text>
|
||||
<Text dimColor>{requiresExtensionSuffix}</Text>
|
||||
</>
|
||||
),
|
||||
value: 'manage-permissions',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<>
|
||||
<Text>Reconnect extension</Text>
|
||||
<Text dimColor>{requiresExtensionSuffix}</Text>
|
||||
</>
|
||||
),
|
||||
value: 'reconnect',
|
||||
},
|
||||
{
|
||||
label: `Enabled by default: ${enabledByDefault ? 'Yes' : 'No'}`,
|
||||
value: 'toggle-default',
|
||||
},
|
||||
)
|
||||
|
||||
const isDisabled =
|
||||
isWSL || ("external" !== 'ant' && !isClaudeAISubscriber)
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="Claude in Chrome (Beta)"
|
||||
onCancel={() => onDone()}
|
||||
color="chromeYellow"
|
||||
>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text>
|
||||
Claude in Chrome works with the Chrome extension to let you control
|
||||
your browser directly from Claude Code. Navigate websites, fill forms,
|
||||
capture screenshots, record GIFs, and debug with console logs and
|
||||
network requests.
|
||||
</Text>
|
||||
|
||||
{isWSL && (
|
||||
<Text color="error">
|
||||
Claude in Chrome is not supported in WSL at this time.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
|
||||
{"external" !== 'ant' && !isClaudeAISubscriber && (
|
||||
<Text color="error">
|
||||
Claude in Chrome requires a claude.ai subscription.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!isDisabled && (
|
||||
<>
|
||||
{!isHomespace && (
|
||||
<Box flexDirection="column">
|
||||
<Text>
|
||||
Status:{' '}
|
||||
{isConnected ? (
|
||||
<Text color="success">Enabled</Text>
|
||||
) : (
|
||||
<Text color="inactive">Disabled</Text>
|
||||
)}
|
||||
</Text>
|
||||
<Text>
|
||||
Extension:{' '}
|
||||
{isExtensionInstalled ? (
|
||||
<Text color="success">Installed</Text>
|
||||
) : (
|
||||
<Text color="warning">Not detected</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Select
|
||||
key={selectKey}
|
||||
options={options}
|
||||
onChange={handleAction}
|
||||
hideIndexes
|
||||
/>
|
||||
|
||||
{showInstallHint && (
|
||||
<Text color="warning">
|
||||
Once installed, select {'"Reconnect extension"'} to connect.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Text>
|
||||
<Text dimColor>Usage: </Text>
|
||||
<Text>claude --chrome</Text>
|
||||
<Text dimColor> or </Text>
|
||||
<Text>claude --no-chrome</Text>
|
||||
</Text>
|
||||
|
||||
<Text dimColor>
|
||||
Site-level permissions are inherited from the Chrome extension.
|
||||
Manage permissions in the Chrome extension settings to control
|
||||
which sites Claude can browse, click, and type on.
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
<Text dimColor>Learn more: https://code.claude.com/docs/en/chrome</Text>
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
function _temp5(k) {
|
||||
return k + 1;
|
||||
|
||||
export const call = async function (
|
||||
onDone: (result?: string) => void,
|
||||
): Promise<React.ReactNode> {
|
||||
const isExtensionInstalled = await isChromeExtensionInstalled()
|
||||
const config = getGlobalConfig()
|
||||
const isSubscriber = isClaudeAISubscriber()
|
||||
const isWSL = env.isWslEnvironment()
|
||||
|
||||
return (
|
||||
<ClaudeInChromeMenu
|
||||
onDone={onDone}
|
||||
isExtensionInstalled={isExtensionInstalled}
|
||||
configEnabled={config.claudeInChromeDefaultEnabled}
|
||||
isClaudeAISubscriber={isSubscriber}
|
||||
isWSL={isWSL}
|
||||
/>
|
||||
)
|
||||
}
|
||||
function _temp4(k_0) {
|
||||
return k_0 + 1;
|
||||
}
|
||||
function _temp3(k_1) {
|
||||
return k_1 + 1;
|
||||
}
|
||||
function _temp2(c) {
|
||||
return c.name === CLAUDE_IN_CHROME_MCP_SERVER_NAME;
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.mcp.clients;
|
||||
}
|
||||
export const call = async function (onDone: (result?: string) => void): Promise<React.ReactNode> {
|
||||
const isExtensionInstalled = await isChromeExtensionInstalled();
|
||||
const config = getGlobalConfig();
|
||||
const isSubscriber = isClaudeAISubscriber();
|
||||
const isWSL = env.isWslEnvironment();
|
||||
return <ClaudeInChromeMenu onDone={onDone} isExtensionInstalled={isExtensionInstalled} configEnabled={config.claudeInChromeDefaultEnabled} isClaudeAISubscriber={isSubscriber} isWSL={isWSL} />;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { Settings } from '../../components/Settings/Settings.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import * as React from 'react'
|
||||
import { Settings } from '../../components/Settings/Settings.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, context) => {
|
||||
return <Settings onClose={onDone} context={context} defaultTab="Config" />;
|
||||
};
|
||||
return <Settings onClose={onDone} context={context} defaultTab="Config" />
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { feature } from 'bun:bundle';
|
||||
import * as React from 'react';
|
||||
import type { LocalJSXCommandContext } from '../../commands.js';
|
||||
import { ContextVisualization } from '../../components/ContextVisualization.js';
|
||||
import { microcompactMessages } from '../../services/compact/microCompact.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import type { Message } from '../../types/message.js';
|
||||
import { analyzeContextUsage } from '../../utils/analyzeContext.js';
|
||||
import { getMessagesAfterCompactBoundary } from '../../utils/messages.js';
|
||||
import { renderToAnsiString } from '../../utils/staticRender.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import * as React from 'react'
|
||||
import type { LocalJSXCommandContext } from '../../commands.js'
|
||||
import { ContextVisualization } from '../../components/ContextVisualization.js'
|
||||
import { microcompactMessages } from '../../services/compact/microCompact.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import type { Message } from '../../types/message.js'
|
||||
import { analyzeContextUsage } from '../../utils/analyzeContext.js'
|
||||
import { getMessagesAfterCompactBoundary } from '../../utils/messages.js'
|
||||
import { renderToAnsiString } from '../../utils/staticRender.js'
|
||||
|
||||
/**
|
||||
* Apply the same context transforms query.ts does before the API call, so
|
||||
@@ -16,48 +16,53 @@ import { renderToAnsiString } from '../../utils/staticRender.js';
|
||||
* was collapsed — user sees "180k, 3 spans collapsed" when the API sees 120k.
|
||||
*/
|
||||
function toApiView(messages: Message[]): Message[] {
|
||||
let view = getMessagesAfterCompactBoundary(messages);
|
||||
let view = getMessagesAfterCompactBoundary(messages)
|
||||
if (feature('CONTEXT_COLLAPSE')) {
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const {
|
||||
projectView
|
||||
} = require('../../services/contextCollapse/operations.js') as typeof import('../../services/contextCollapse/operations.js');
|
||||
const { projectView } =
|
||||
require('../../services/contextCollapse/operations.js') as typeof import('../../services/contextCollapse/operations.js')
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
view = projectView(view);
|
||||
view = projectView(view)
|
||||
}
|
||||
return view;
|
||||
return view
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext): Promise<React.ReactNode> {
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
): Promise<React.ReactNode> {
|
||||
const {
|
||||
messages,
|
||||
getAppState,
|
||||
options: {
|
||||
mainLoopModel,
|
||||
tools
|
||||
}
|
||||
} = context;
|
||||
const apiView = toApiView(messages);
|
||||
options: { mainLoopModel, tools },
|
||||
} = context
|
||||
|
||||
const apiView = toApiView(messages)
|
||||
|
||||
// Apply microcompact to get accurate representation of messages sent to API
|
||||
const {
|
||||
messages: compactedMessages
|
||||
} = await microcompactMessages(apiView);
|
||||
const { messages: compactedMessages } = await microcompactMessages(apiView)
|
||||
|
||||
// Get terminal width for responsive sizing
|
||||
const terminalWidth = process.stdout.columns || 80;
|
||||
const appState = getAppState();
|
||||
const terminalWidth = process.stdout.columns || 80
|
||||
|
||||
const appState = getAppState()
|
||||
|
||||
// Analyze context with compacted messages
|
||||
// Pass original messages as last parameter for accurate API usage extraction
|
||||
const data = await analyzeContextUsage(compactedMessages, mainLoopModel, async () => appState.toolPermissionContext, tools, appState.agentDefinitions, terminalWidth, context,
|
||||
// Pass full context for system prompt calculation
|
||||
undefined,
|
||||
// mainThreadAgentDefinition
|
||||
apiView // Original messages for API usage extraction
|
||||
);
|
||||
const data = await analyzeContextUsage(
|
||||
compactedMessages,
|
||||
mainLoopModel,
|
||||
async () => appState.toolPermissionContext,
|
||||
tools,
|
||||
appState.agentDefinitions,
|
||||
terminalWidth,
|
||||
context, // Pass full context for system prompt calculation
|
||||
undefined, // mainThreadAgentDefinition
|
||||
apiView, // Original messages for API usage extraction
|
||||
)
|
||||
|
||||
// Render to ANSI string to preserve colors and pass to onDone like local commands do
|
||||
const output = await renderToAnsiString(<ContextVisualization data={data} />);
|
||||
onDone(output);
|
||||
return null;
|
||||
const output = await renderToAnsiString(<ContextVisualization data={data} />)
|
||||
onDone(output)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,45 +1,44 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { mkdir, writeFile } from 'fs/promises';
|
||||
import { marked, type Tokens } from 'marked';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import React, { useRef } from 'react';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import type { OptionWithDescription } from '../../components/CustomSelect/select.js';
|
||||
import { Select } from '../../components/CustomSelect/select.js';
|
||||
import { Byline } from '../../components/design-system/Byline.js';
|
||||
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js';
|
||||
import { Pane } from '../../components/design-system/Pane.js';
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||||
import { stringWidth } from '../../ink/stringWidth.js';
|
||||
import { setClipboard } from '../../ink/termio/osc.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { logEvent } from '../../services/analytics/index.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import type { AssistantMessage, Message } from '../../types/message.js';
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
|
||||
import { extractTextContent, stripPromptXMLTags } from '../../utils/messages.js';
|
||||
import { countCharInString } from '../../utils/stringUtils.js';
|
||||
const COPY_DIR = join(tmpdir(), 'claude');
|
||||
const RESPONSE_FILENAME = 'response.md';
|
||||
const MAX_LOOKBACK = 20;
|
||||
import { mkdir, writeFile } from 'fs/promises'
|
||||
import { marked, type Tokens } from 'marked'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import React, { useRef } from 'react'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import type { OptionWithDescription } from '../../components/CustomSelect/select.js'
|
||||
import { Select } from '../../components/CustomSelect/select.js'
|
||||
import { Byline } from '../../components/design-system/Byline.js'
|
||||
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js'
|
||||
import { Pane } from '../../components/design-system/Pane.js'
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||
import { stringWidth } from '../../ink/stringWidth.js'
|
||||
import { setClipboard } from '../../ink/termio/osc.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { logEvent } from '../../services/analytics/index.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
import type { AssistantMessage, Message } from '../../types/message.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||
import { extractTextContent, stripPromptXMLTags } from '../../utils/messages.js'
|
||||
import { countCharInString } from '../../utils/stringUtils.js'
|
||||
|
||||
const COPY_DIR = join(tmpdir(), 'claude')
|
||||
const RESPONSE_FILENAME = 'response.md'
|
||||
const MAX_LOOKBACK = 20
|
||||
|
||||
type CodeBlock = {
|
||||
code: string;
|
||||
lang: string | undefined;
|
||||
};
|
||||
code: string
|
||||
lang: string | undefined
|
||||
}
|
||||
|
||||
function extractCodeBlocks(markdown: string): CodeBlock[] {
|
||||
const tokens = marked.lexer(stripPromptXMLTags(markdown));
|
||||
const blocks: CodeBlock[] = [];
|
||||
const tokens = marked.lexer(stripPromptXMLTags(markdown))
|
||||
const blocks: CodeBlock[] = []
|
||||
for (const token of tokens) {
|
||||
if (token.type === 'code') {
|
||||
const codeToken = token as Tokens.Code;
|
||||
blocks.push({
|
||||
code: codeToken.text,
|
||||
lang: codeToken.lang
|
||||
});
|
||||
const codeToken = token as Tokens.Code
|
||||
blocks.push({ code: codeToken.text, lang: codeToken.lang })
|
||||
}
|
||||
}
|
||||
return blocks;
|
||||
return blocks
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,323 +47,267 @@ function extractCodeBlocks(markdown: string): CodeBlock[] {
|
||||
* Index 0 = latest, 1 = second-to-latest, etc. Caps at MAX_LOOKBACK.
|
||||
*/
|
||||
export function collectRecentAssistantTexts(messages: Message[]): string[] {
|
||||
const texts: string[] = [];
|
||||
for (let i = messages.length - 1; i >= 0 && texts.length < MAX_LOOKBACK; i--) {
|
||||
const msg = messages[i];
|
||||
if (msg?.type !== 'assistant' || msg.isApiErrorMessage) continue;
|
||||
const content = (msg as AssistantMessage).message.content;
|
||||
if (!Array.isArray(content)) continue;
|
||||
const text = extractTextContent(content, '\n\n');
|
||||
if (text) texts.push(text);
|
||||
const texts: string[] = []
|
||||
for (
|
||||
let i = messages.length - 1;
|
||||
i >= 0 && texts.length < MAX_LOOKBACK;
|
||||
i--
|
||||
) {
|
||||
const msg = messages[i]
|
||||
if (msg?.type !== 'assistant' || msg.isApiErrorMessage) continue
|
||||
const content = (msg as AssistantMessage).message.content
|
||||
if (!Array.isArray(content)) continue
|
||||
const text = extractTextContent(content, '\n\n')
|
||||
if (text) texts.push(text)
|
||||
}
|
||||
return texts;
|
||||
return texts
|
||||
}
|
||||
|
||||
export function fileExtension(lang: string | undefined): string {
|
||||
if (lang) {
|
||||
// Sanitize to prevent path traversal (e.g. ```../../etc/passwd)
|
||||
// Language identifiers are alphanumeric: python, tsx, jsonc, etc.
|
||||
const sanitized = lang.replace(/[^a-zA-Z0-9]/g, '');
|
||||
const sanitized = lang.replace(/[^a-zA-Z0-9]/g, '')
|
||||
if (sanitized && sanitized !== 'plaintext') {
|
||||
return `.${sanitized}`;
|
||||
return `.${sanitized}`
|
||||
}
|
||||
}
|
||||
return '.txt';
|
||||
return '.txt'
|
||||
}
|
||||
|
||||
async function writeToFile(text: string, filename: string): Promise<string> {
|
||||
const filePath = join(COPY_DIR, filename);
|
||||
await mkdir(COPY_DIR, {
|
||||
recursive: true
|
||||
});
|
||||
await writeFile(filePath, text, 'utf-8');
|
||||
return filePath;
|
||||
const filePath = join(COPY_DIR, filename)
|
||||
await mkdir(COPY_DIR, { recursive: true })
|
||||
await writeFile(filePath, text, 'utf-8')
|
||||
return filePath
|
||||
}
|
||||
async function copyOrWriteToFile(text: string, filename: string): Promise<string> {
|
||||
const raw = await setClipboard(text);
|
||||
if (raw) process.stdout.write(raw);
|
||||
const lineCount = countCharInString(text, '\n') + 1;
|
||||
const charCount = text.length;
|
||||
|
||||
async function copyOrWriteToFile(
|
||||
text: string,
|
||||
filename: string,
|
||||
): Promise<string> {
|
||||
const raw = await setClipboard(text)
|
||||
if (raw) process.stdout.write(raw)
|
||||
const lineCount = countCharInString(text, '\n') + 1
|
||||
const charCount = text.length
|
||||
// Also write to a temp file — clipboard paths are best-effort (OSC 52 needs
|
||||
// terminal support), so the file provides a reliable fallback.
|
||||
try {
|
||||
const filePath = await writeToFile(text, filename);
|
||||
return `Copied to clipboard (${charCount} characters, ${lineCount} lines)\nAlso written to ${filePath}`;
|
||||
const filePath = await writeToFile(text, filename)
|
||||
return `Copied to clipboard (${charCount} characters, ${lineCount} lines)\nAlso written to ${filePath}`
|
||||
} catch {
|
||||
return `Copied to clipboard (${charCount} characters, ${lineCount} lines)`;
|
||||
return `Copied to clipboard (${charCount} characters, ${lineCount} lines)`
|
||||
}
|
||||
}
|
||||
|
||||
function truncateLine(text: string, maxLen: number): string {
|
||||
const firstLine = text.split('\n')[0] ?? '';
|
||||
const firstLine = text.split('\n')[0] ?? ''
|
||||
if (stringWidth(firstLine) <= maxLen) {
|
||||
return firstLine;
|
||||
return firstLine
|
||||
}
|
||||
let result = '';
|
||||
let width = 0;
|
||||
const targetWidth = maxLen - 1;
|
||||
let result = ''
|
||||
let width = 0
|
||||
const targetWidth = maxLen - 1
|
||||
for (const char of firstLine) {
|
||||
const charWidth = stringWidth(char);
|
||||
if (width + charWidth > targetWidth) break;
|
||||
result += char;
|
||||
width += charWidth;
|
||||
const charWidth = stringWidth(char)
|
||||
if (width + charWidth > targetWidth) break
|
||||
result += char
|
||||
width += charWidth
|
||||
}
|
||||
return result + '\u2026';
|
||||
return result + '\u2026'
|
||||
}
|
||||
|
||||
type PickerProps = {
|
||||
fullText: string;
|
||||
codeBlocks: CodeBlock[];
|
||||
messageAge: number;
|
||||
onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
};
|
||||
type PickerSelection = number | 'full' | 'always';
|
||||
function CopyPicker(t0) {
|
||||
const $ = _c(33);
|
||||
const {
|
||||
fullText,
|
||||
codeBlocks,
|
||||
messageAge,
|
||||
onDone
|
||||
} = t0;
|
||||
const focusedRef = useRef("full");
|
||||
const t1 = `${fullText.length} chars, ${countCharInString(fullText, "\n") + 1} lines`;
|
||||
let t2;
|
||||
if ($[0] !== t1) {
|
||||
t2 = {
|
||||
label: "Full response",
|
||||
value: "full" as const,
|
||||
description: t1
|
||||
};
|
||||
$[0] = t1;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
let t3;
|
||||
if ($[2] !== codeBlocks || $[3] !== t2) {
|
||||
let t4;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = {
|
||||
label: "Always copy full response",
|
||||
value: "always" as const,
|
||||
description: "Skip this picker in the future (revert via /config)"
|
||||
};
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
t3 = [t2, ...codeBlocks.map(_temp), t4];
|
||||
$[2] = codeBlocks;
|
||||
$[3] = t2;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
const options = t3;
|
||||
let t4;
|
||||
if ($[6] !== codeBlocks || $[7] !== fullText) {
|
||||
t4 = function getSelectionContent(selected) {
|
||||
if (selected === "full" || selected === "always") {
|
||||
return {
|
||||
text: fullText,
|
||||
filename: RESPONSE_FILENAME
|
||||
};
|
||||
}
|
||||
const block_0 = codeBlocks[selected];
|
||||
fullText: string
|
||||
codeBlocks: CodeBlock[]
|
||||
messageAge: number
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
}
|
||||
|
||||
type PickerSelection = number | 'full' | 'always'
|
||||
|
||||
function CopyPicker({
|
||||
fullText,
|
||||
codeBlocks,
|
||||
messageAge,
|
||||
onDone,
|
||||
}: PickerProps): React.ReactNode {
|
||||
const focusedRef = useRef<PickerSelection>('full')
|
||||
|
||||
const options: OptionWithDescription<PickerSelection>[] = [
|
||||
{
|
||||
label: 'Full response',
|
||||
value: 'full' as const,
|
||||
description: `${fullText.length} chars, ${countCharInString(fullText, '\n') + 1} lines`,
|
||||
},
|
||||
...codeBlocks.map((block, index) => {
|
||||
const blockLines = countCharInString(block.code, '\n') + 1
|
||||
return {
|
||||
text: block_0.code,
|
||||
filename: `copy${fileExtension(block_0.lang)}`,
|
||||
blockIndex: selected
|
||||
};
|
||||
};
|
||||
$[6] = codeBlocks;
|
||||
$[7] = fullText;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
const getSelectionContent = t4;
|
||||
let t5;
|
||||
if ($[9] !== codeBlocks.length || $[10] !== getSelectionContent || $[11] !== messageAge || $[12] !== onDone) {
|
||||
t5 = async function handleSelect(selected_0) {
|
||||
const content = getSelectionContent(selected_0);
|
||||
if (selected_0 === "always") {
|
||||
if (!getGlobalConfig().copyFullResponse) {
|
||||
saveGlobalConfig(_temp2);
|
||||
}
|
||||
logEvent("tengu_copy", {
|
||||
block_count: codeBlocks.length,
|
||||
always: true,
|
||||
message_age: messageAge
|
||||
});
|
||||
const result = await copyOrWriteToFile(content.text, content.filename);
|
||||
onDone(`${result}\nPreference saved. Use /config to change copyFullResponse`);
|
||||
return;
|
||||
label: truncateLine(block.code, 60),
|
||||
value: index,
|
||||
description:
|
||||
[block.lang, blockLines > 1 ? `${blockLines} lines` : undefined]
|
||||
.filter(Boolean)
|
||||
.join(', ') || undefined,
|
||||
}
|
||||
logEvent("tengu_copy", {
|
||||
selected_block: content.blockIndex,
|
||||
block_count: codeBlocks.length,
|
||||
message_age: messageAge
|
||||
});
|
||||
const result_0 = await copyOrWriteToFile(content.text, content.filename);
|
||||
onDone(result_0);
|
||||
};
|
||||
$[9] = codeBlocks.length;
|
||||
$[10] = getSelectionContent;
|
||||
$[11] = messageAge;
|
||||
$[12] = onDone;
|
||||
$[13] = t5;
|
||||
} else {
|
||||
t5 = $[13];
|
||||
}),
|
||||
{
|
||||
label: 'Always copy full response',
|
||||
value: 'always' as const,
|
||||
description: 'Skip this picker in the future (revert via /config)',
|
||||
},
|
||||
]
|
||||
|
||||
function getSelectionContent(selected: PickerSelection): {
|
||||
text: string
|
||||
filename: string
|
||||
blockIndex?: number
|
||||
} {
|
||||
if (selected === 'full' || selected === 'always') {
|
||||
return { text: fullText, filename: RESPONSE_FILENAME }
|
||||
}
|
||||
const block = codeBlocks[selected]!
|
||||
return {
|
||||
text: block.code,
|
||||
filename: `copy${fileExtension(block.lang)}`,
|
||||
blockIndex: selected,
|
||||
}
|
||||
}
|
||||
const handleSelect = t5;
|
||||
let t6;
|
||||
if ($[14] !== codeBlocks.length || $[15] !== getSelectionContent || $[16] !== messageAge || $[17] !== onDone) {
|
||||
const handleWrite = async function handleWrite(selected_1) {
|
||||
const content_0 = getSelectionContent(selected_1);
|
||||
logEvent("tengu_copy", {
|
||||
selected_block: content_0.blockIndex,
|
||||
|
||||
async function handleSelect(selected: PickerSelection): Promise<void> {
|
||||
const content = getSelectionContent(selected)
|
||||
if (selected === 'always') {
|
||||
if (!getGlobalConfig().copyFullResponse) {
|
||||
saveGlobalConfig(c => ({ ...c, copyFullResponse: true }))
|
||||
}
|
||||
logEvent('tengu_copy', {
|
||||
block_count: codeBlocks.length,
|
||||
always: true,
|
||||
message_age: messageAge,
|
||||
write_shortcut: true
|
||||
});
|
||||
;
|
||||
try {
|
||||
const filePath = await writeToFile(content_0.text, content_0.filename);
|
||||
onDone(`Written to ${filePath}`);
|
||||
} catch (t7) {
|
||||
const e = t7;
|
||||
onDone(`Failed to write file: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
};
|
||||
t6 = function handleKeyDown(e_0) {
|
||||
if (e_0.key === "w") {
|
||||
e_0.preventDefault();
|
||||
handleWrite(focusedRef.current);
|
||||
}
|
||||
};
|
||||
$[14] = codeBlocks.length;
|
||||
$[15] = getSelectionContent;
|
||||
$[16] = messageAge;
|
||||
$[17] = onDone;
|
||||
$[18] = t6;
|
||||
} else {
|
||||
t6 = $[18];
|
||||
})
|
||||
const result = await copyOrWriteToFile(content.text, content.filename)
|
||||
onDone(
|
||||
`${result}\nPreference saved. Use /config to change copyFullResponse`,
|
||||
)
|
||||
return
|
||||
}
|
||||
logEvent('tengu_copy', {
|
||||
selected_block: content.blockIndex,
|
||||
block_count: codeBlocks.length,
|
||||
message_age: messageAge,
|
||||
})
|
||||
const result = await copyOrWriteToFile(content.text, content.filename)
|
||||
onDone(result)
|
||||
}
|
||||
const handleKeyDown = t6;
|
||||
let t7;
|
||||
if ($[19] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Text dimColor={true}>Select content to copy:</Text>;
|
||||
$[19] = t7;
|
||||
} else {
|
||||
t7 = $[19];
|
||||
|
||||
async function handleWrite(selected: PickerSelection): Promise<void> {
|
||||
const content = getSelectionContent(selected)
|
||||
logEvent('tengu_copy', {
|
||||
selected_block: content.blockIndex,
|
||||
block_count: codeBlocks.length,
|
||||
message_age: messageAge,
|
||||
write_shortcut: true,
|
||||
})
|
||||
try {
|
||||
const filePath = await writeToFile(content.text, content.filename)
|
||||
onDone(`Written to ${filePath}`)
|
||||
} catch (e) {
|
||||
onDone(`Failed to write file: ${e instanceof Error ? e.message : e}`)
|
||||
}
|
||||
}
|
||||
let t8;
|
||||
if ($[20] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = value => {
|
||||
focusedRef.current = value;
|
||||
};
|
||||
$[20] = t8;
|
||||
} else {
|
||||
t8 = $[20];
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent): void {
|
||||
if (e.key === 'w') {
|
||||
e.preventDefault()
|
||||
void handleWrite(focusedRef.current)
|
||||
}
|
||||
}
|
||||
let t9;
|
||||
if ($[21] !== handleSelect) {
|
||||
t9 = selected_2 => {
|
||||
handleSelect(selected_2);
|
||||
};
|
||||
$[21] = handleSelect;
|
||||
$[22] = t9;
|
||||
} else {
|
||||
t9 = $[22];
|
||||
}
|
||||
let t10;
|
||||
if ($[23] !== onDone) {
|
||||
t10 = () => {
|
||||
onDone("Copy cancelled", {
|
||||
display: "system"
|
||||
});
|
||||
};
|
||||
$[23] = onDone;
|
||||
$[24] = t10;
|
||||
} else {
|
||||
t10 = $[24];
|
||||
}
|
||||
let t11;
|
||||
if ($[25] !== options || $[26] !== t10 || $[27] !== t9) {
|
||||
t11 = <Select options={options} hideIndexes={false} onFocus={t8} onChange={t9} onCancel={t10} />;
|
||||
$[25] = options;
|
||||
$[26] = t10;
|
||||
$[27] = t9;
|
||||
$[28] = t11;
|
||||
} else {
|
||||
t11 = $[28];
|
||||
}
|
||||
let t12;
|
||||
if ($[29] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t12 = <Text dimColor={true}><Byline><KeyboardShortcutHint shortcut="enter" action="copy" /><KeyboardShortcutHint shortcut="w" action="write to file" /><KeyboardShortcutHint shortcut="esc" action="cancel" /></Byline></Text>;
|
||||
$[29] = t12;
|
||||
} else {
|
||||
t12 = $[29];
|
||||
}
|
||||
let t13;
|
||||
if ($[30] !== handleKeyDown || $[31] !== t11) {
|
||||
t13 = <Pane><Box flexDirection="column" gap={1} tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t7}{t11}{t12}</Box></Pane>;
|
||||
$[30] = handleKeyDown;
|
||||
$[31] = t11;
|
||||
$[32] = t13;
|
||||
} else {
|
||||
t13 = $[32];
|
||||
}
|
||||
return t13;
|
||||
}
|
||||
function _temp2(c) {
|
||||
return {
|
||||
...c,
|
||||
copyFullResponse: true
|
||||
};
|
||||
}
|
||||
function _temp(block, index) {
|
||||
const blockLines = countCharInString(block.code, "\n") + 1;
|
||||
return {
|
||||
label: truncateLine(block.code, 60),
|
||||
value: index,
|
||||
description: [block.lang, blockLines > 1 ? `${blockLines} lines` : undefined].filter(Boolean).join(", ") || undefined
|
||||
};
|
||||
|
||||
return (
|
||||
<Pane>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
gap={1}
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Text dimColor>Select content to copy:</Text>
|
||||
<Select<PickerSelection>
|
||||
options={options}
|
||||
hideIndexes={false}
|
||||
onFocus={value => {
|
||||
focusedRef.current = value
|
||||
}}
|
||||
onChange={selected => {
|
||||
void handleSelect(selected)
|
||||
}}
|
||||
onCancel={() => {
|
||||
onDone('Copy cancelled', { display: 'system' })
|
||||
}}
|
||||
/>
|
||||
<Text dimColor>
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="enter" action="copy" />
|
||||
<KeyboardShortcutHint shortcut="w" action="write to file" />
|
||||
<KeyboardShortcutHint shortcut="esc" action="cancel" />
|
||||
</Byline>
|
||||
</Text>
|
||||
</Box>
|
||||
</Pane>
|
||||
)
|
||||
}
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, context, args) => {
|
||||
const texts = collectRecentAssistantTexts(context.messages);
|
||||
const texts = collectRecentAssistantTexts(context.messages)
|
||||
|
||||
if (texts.length === 0) {
|
||||
onDone('No assistant message to copy');
|
||||
return null;
|
||||
onDone('No assistant message to copy')
|
||||
return null
|
||||
}
|
||||
|
||||
// /copy N reaches back N-1 messages (1 = latest, 2 = second-to-latest, ...)
|
||||
let age = 0;
|
||||
const arg = args?.trim();
|
||||
let age = 0
|
||||
const arg = args?.trim()
|
||||
if (arg) {
|
||||
const n = Number(arg);
|
||||
const n = Number(arg)
|
||||
if (!Number.isInteger(n) || n < 1) {
|
||||
onDone(`Usage: /copy [N] where N is 1 (latest), 2, 3, \u2026 Got: ${arg}`);
|
||||
return null;
|
||||
onDone(`Usage: /copy [N] where N is 1 (latest), 2, 3, \u2026 Got: ${arg}`)
|
||||
return null
|
||||
}
|
||||
if (n > texts.length) {
|
||||
onDone(`Only ${texts.length} assistant ${texts.length === 1 ? 'message' : 'messages'} available to copy`);
|
||||
return null;
|
||||
onDone(
|
||||
`Only ${texts.length} assistant ${texts.length === 1 ? 'message' : 'messages'} available to copy`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
age = n - 1;
|
||||
age = n - 1
|
||||
}
|
||||
const text = texts[age]!;
|
||||
const codeBlocks = extractCodeBlocks(text);
|
||||
const config = getGlobalConfig();
|
||||
|
||||
const text = texts[age]!
|
||||
const codeBlocks = extractCodeBlocks(text)
|
||||
const config = getGlobalConfig()
|
||||
|
||||
if (codeBlocks.length === 0 || config.copyFullResponse) {
|
||||
logEvent('tengu_copy', {
|
||||
always: config.copyFullResponse,
|
||||
block_count: codeBlocks.length,
|
||||
message_age: age
|
||||
});
|
||||
const result = await copyOrWriteToFile(text, RESPONSE_FILENAME);
|
||||
onDone(result);
|
||||
return null;
|
||||
message_age: age,
|
||||
})
|
||||
const result = await copyOrWriteToFile(text, RESPONSE_FILENAME)
|
||||
onDone(result)
|
||||
return null
|
||||
}
|
||||
return <CopyPicker fullText={text} codeBlocks={codeBlocks} messageAge={age} onDone={onDone} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<CopyPicker
|
||||
fullText={text}
|
||||
codeBlocks={codeBlocks}
|
||||
messageAge={age}
|
||||
onDone={onDone}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import React from 'react';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import { DesktopHandoff } from '../../components/DesktopHandoff.js';
|
||||
export async function call(onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void): Promise<React.ReactNode> {
|
||||
return <DesktopHandoff onDone={onDone} />;
|
||||
import React from 'react'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import { DesktopHandoff } from '../../components/DesktopHandoff.js'
|
||||
|
||||
export async function call(
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void,
|
||||
): Promise<React.ReactNode> {
|
||||
return <DesktopHandoff onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import * as React from 'react'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, context) => {
|
||||
const {
|
||||
DiffDialog
|
||||
} = await import('../../components/diff/DiffDialog.js');
|
||||
return <DiffDialog messages={context.messages} onDone={onDone} />;
|
||||
};
|
||||
const { DiffDialog } = await import('../../components/diff/DiffDialog.js')
|
||||
return <DiffDialog messages={context.messages} onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Doctor } from '../../screens/Doctor.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import React from 'react'
|
||||
import { Doctor } from '../../screens/Doctor.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
|
||||
export const call: LocalJSXCommandCall = (onDone, _context, _args) => {
|
||||
return Promise.resolve(<Doctor onDone={onDone} />);
|
||||
};
|
||||
return Promise.resolve(<Doctor onDone={onDone} />)
|
||||
}
|
||||
|
||||
@@ -1,182 +1,183 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js';
|
||||
import { useAppState, useSetAppState } from '../../state/AppState.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { type EffortValue, getDisplayedEffortLevel, getEffortEnvOverride, getEffortValueDescription, isEffortLevel, toPersistableEffort } from '../../utils/effort.js';
|
||||
import { updateSettingsForSource } from '../../utils/settings/settings.js';
|
||||
const COMMON_HELP_ARGS = ['help', '-h', '--help'];
|
||||
import * as React from 'react'
|
||||
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
import { useAppState, useSetAppState } from '../../state/AppState.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import {
|
||||
type EffortValue,
|
||||
getDisplayedEffortLevel,
|
||||
getEffortEnvOverride,
|
||||
getEffortValueDescription,
|
||||
isEffortLevel,
|
||||
toPersistableEffort,
|
||||
} from '../../utils/effort.js'
|
||||
import { updateSettingsForSource } from '../../utils/settings/settings.js'
|
||||
|
||||
const COMMON_HELP_ARGS = ['help', '-h', '--help']
|
||||
|
||||
type EffortCommandResult = {
|
||||
message: string;
|
||||
effortUpdate?: {
|
||||
value: EffortValue | undefined;
|
||||
};
|
||||
};
|
||||
message: string
|
||||
effortUpdate?: { value: EffortValue | undefined }
|
||||
}
|
||||
|
||||
function setEffortValue(effortValue: EffortValue): EffortCommandResult {
|
||||
const persistable = toPersistableEffort(effortValue);
|
||||
const persistable = toPersistableEffort(effortValue)
|
||||
if (persistable !== undefined) {
|
||||
const result = updateSettingsForSource('userSettings', {
|
||||
effortLevel: persistable
|
||||
});
|
||||
effortLevel: persistable,
|
||||
})
|
||||
if (result.error) {
|
||||
return {
|
||||
message: `Failed to set effort level: ${result.error.message}`
|
||||
};
|
||||
message: `Failed to set effort level: ${result.error.message}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
logEvent('tengu_effort_command', {
|
||||
effort: effortValue as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
effort:
|
||||
effortValue as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
|
||||
// Env var wins at resolveAppliedEffort time. Only flag it when it actually
|
||||
// conflicts — if env matches what the user just asked for, the outcome is
|
||||
// the same, so "Set effort to X" is true and the note is noise.
|
||||
const envOverride = getEffortEnvOverride();
|
||||
const envOverride = getEffortEnvOverride()
|
||||
if (envOverride !== undefined && envOverride !== effortValue) {
|
||||
const envRaw = process.env.CLAUDE_CODE_EFFORT_LEVEL;
|
||||
const envRaw = process.env.CLAUDE_CODE_EFFORT_LEVEL
|
||||
if (persistable === undefined) {
|
||||
return {
|
||||
message: `Not applied: CLAUDE_CODE_EFFORT_LEVEL=${envRaw} overrides effort this session, and ${effortValue} is session-only (nothing saved)`,
|
||||
effortUpdate: {
|
||||
value: effortValue
|
||||
}
|
||||
};
|
||||
effortUpdate: { value: effortValue },
|
||||
}
|
||||
}
|
||||
return {
|
||||
message: `CLAUDE_CODE_EFFORT_LEVEL=${envRaw} overrides this session — clear it and ${effortValue} takes over`,
|
||||
effortUpdate: {
|
||||
value: effortValue
|
||||
}
|
||||
};
|
||||
effortUpdate: { value: effortValue },
|
||||
}
|
||||
}
|
||||
const description = getEffortValueDescription(effortValue);
|
||||
const suffix = persistable !== undefined ? '' : ' (this session only)';
|
||||
|
||||
const description = getEffortValueDescription(effortValue)
|
||||
const suffix = persistable !== undefined ? '' : ' (this session only)'
|
||||
return {
|
||||
message: `Set effort level to ${effortValue}${suffix}: ${description}`,
|
||||
effortUpdate: {
|
||||
value: effortValue
|
||||
}
|
||||
};
|
||||
}
|
||||
export function showCurrentEffort(appStateEffort: EffortValue | undefined, model: string): EffortCommandResult {
|
||||
const envOverride = getEffortEnvOverride();
|
||||
const effectiveValue = envOverride === null ? undefined : envOverride ?? appStateEffort;
|
||||
if (effectiveValue === undefined) {
|
||||
const level = getDisplayedEffortLevel(model, appStateEffort);
|
||||
return {
|
||||
message: `Effort level: auto (currently ${level})`
|
||||
};
|
||||
effortUpdate: { value: effortValue },
|
||||
}
|
||||
const description = getEffortValueDescription(effectiveValue);
|
||||
return {
|
||||
message: `Current effort level: ${effectiveValue} (${description})`
|
||||
};
|
||||
}
|
||||
|
||||
export function showCurrentEffort(
|
||||
appStateEffort: EffortValue | undefined,
|
||||
model: string,
|
||||
): EffortCommandResult {
|
||||
const envOverride = getEffortEnvOverride()
|
||||
const effectiveValue =
|
||||
envOverride === null ? undefined : (envOverride ?? appStateEffort)
|
||||
if (effectiveValue === undefined) {
|
||||
const level = getDisplayedEffortLevel(model, appStateEffort)
|
||||
return { message: `Effort level: auto (currently ${level})` }
|
||||
}
|
||||
const description = getEffortValueDescription(effectiveValue)
|
||||
return {
|
||||
message: `Current effort level: ${effectiveValue} (${description})`,
|
||||
}
|
||||
}
|
||||
|
||||
function unsetEffortLevel(): EffortCommandResult {
|
||||
const result = updateSettingsForSource('userSettings', {
|
||||
effortLevel: undefined
|
||||
});
|
||||
effortLevel: undefined,
|
||||
})
|
||||
if (result.error) {
|
||||
return {
|
||||
message: `Failed to set effort level: ${result.error.message}`
|
||||
};
|
||||
message: `Failed to set effort level: ${result.error.message}`,
|
||||
}
|
||||
}
|
||||
logEvent('tengu_effort_command', {
|
||||
effort: 'auto' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
effort:
|
||||
'auto' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
// env=auto/unset (null) matches what /effort auto asks for, so only warn
|
||||
// when env is pinning a specific level that will keep overriding.
|
||||
const envOverride = getEffortEnvOverride();
|
||||
const envOverride = getEffortEnvOverride()
|
||||
if (envOverride !== undefined && envOverride !== null) {
|
||||
const envRaw = process.env.CLAUDE_CODE_EFFORT_LEVEL;
|
||||
const envRaw = process.env.CLAUDE_CODE_EFFORT_LEVEL
|
||||
return {
|
||||
message: `Cleared effort from settings, but CLAUDE_CODE_EFFORT_LEVEL=${envRaw} still controls this session`,
|
||||
effortUpdate: {
|
||||
value: undefined
|
||||
}
|
||||
};
|
||||
effortUpdate: { value: undefined },
|
||||
}
|
||||
}
|
||||
return {
|
||||
message: 'Effort level set to auto',
|
||||
effortUpdate: {
|
||||
value: undefined
|
||||
}
|
||||
};
|
||||
}
|
||||
export function executeEffort(args: string): EffortCommandResult {
|
||||
const normalized = args.toLowerCase();
|
||||
if (normalized === 'auto' || normalized === 'unset') {
|
||||
return unsetEffortLevel();
|
||||
effortUpdate: { value: undefined },
|
||||
}
|
||||
}
|
||||
|
||||
export function executeEffort(args: string): EffortCommandResult {
|
||||
const normalized = args.toLowerCase()
|
||||
if (normalized === 'auto' || normalized === 'unset') {
|
||||
return unsetEffortLevel()
|
||||
}
|
||||
|
||||
if (!isEffortLevel(normalized)) {
|
||||
return {
|
||||
message: `Invalid argument: ${args}. Valid options are: low, medium, high, max, auto`
|
||||
};
|
||||
message: `Invalid argument: ${args}. Valid options are: low, medium, high, max, auto`,
|
||||
}
|
||||
}
|
||||
return setEffortValue(normalized);
|
||||
|
||||
return setEffortValue(normalized)
|
||||
}
|
||||
function ShowCurrentEffort(t0) {
|
||||
const {
|
||||
onDone
|
||||
} = t0;
|
||||
const effortValue = useAppState(_temp);
|
||||
const model = useMainLoopModel();
|
||||
const {
|
||||
message
|
||||
} = showCurrentEffort(effortValue, model);
|
||||
onDone(message);
|
||||
return null;
|
||||
|
||||
function ShowCurrentEffort({
|
||||
onDone,
|
||||
}: {
|
||||
onDone: (result: string) => void
|
||||
}): React.ReactNode {
|
||||
const effortValue = useAppState(s => s.effortValue)
|
||||
const model = useMainLoopModel()
|
||||
const { message } = showCurrentEffort(effortValue, model)
|
||||
onDone(message)
|
||||
return null
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.effortValue;
|
||||
|
||||
function ApplyEffortAndClose({
|
||||
result,
|
||||
onDone,
|
||||
}: {
|
||||
result: EffortCommandResult
|
||||
onDone: (result: string) => void
|
||||
}): React.ReactNode {
|
||||
const setAppState = useSetAppState()
|
||||
const { effortUpdate, message } = result
|
||||
React.useEffect(() => {
|
||||
if (effortUpdate) {
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
effortValue: effortUpdate.value,
|
||||
}))
|
||||
}
|
||||
onDone(message)
|
||||
}, [setAppState, effortUpdate, message, onDone])
|
||||
return null
|
||||
}
|
||||
function ApplyEffortAndClose(t0) {
|
||||
const $ = _c(6);
|
||||
const {
|
||||
result,
|
||||
onDone
|
||||
} = t0;
|
||||
const setAppState = useSetAppState();
|
||||
const {
|
||||
effortUpdate,
|
||||
message
|
||||
} = result;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== effortUpdate || $[1] !== message || $[2] !== onDone || $[3] !== setAppState) {
|
||||
t1 = () => {
|
||||
if (effortUpdate) {
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
effortValue: effortUpdate.value
|
||||
}));
|
||||
}
|
||||
onDone(message);
|
||||
};
|
||||
t2 = [setAppState, effortUpdate, message, onDone];
|
||||
$[0] = effortUpdate;
|
||||
$[1] = message;
|
||||
$[2] = onDone;
|
||||
$[3] = setAppState;
|
||||
$[4] = t1;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
t2 = $[5];
|
||||
}
|
||||
React.useEffect(t1, t2);
|
||||
return null;
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, _context: unknown, args?: string): Promise<React.ReactNode> {
|
||||
args = args?.trim() || '';
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
_context: unknown,
|
||||
args?: string,
|
||||
): Promise<React.ReactNode> {
|
||||
args = args?.trim() || ''
|
||||
|
||||
if (COMMON_HELP_ARGS.includes(args)) {
|
||||
onDone('Usage: /effort [low|medium|high|max|auto]\n\nEffort levels:\n- low: Quick, straightforward implementation\n- medium: Balanced approach with standard testing\n- high: Comprehensive implementation with extensive testing\n- max: Maximum capability with deepest reasoning (Opus 4.6 only)\n- auto: Use the default effort level for your model');
|
||||
return;
|
||||
onDone(
|
||||
'Usage: /effort [low|medium|high|max|auto]\n\nEffort levels:\n- low: Quick, straightforward implementation\n- medium: Balanced approach with standard testing\n- high: Comprehensive implementation with extensive testing\n- max: Maximum capability with deepest reasoning (Opus 4.6 only)\n- auto: Use the default effort level for your model',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!args || args === 'current' || args === 'status') {
|
||||
return <ShowCurrentEffort onDone={onDone} />;
|
||||
return <ShowCurrentEffort onDone={onDone} />
|
||||
}
|
||||
const result = executeEffort(args);
|
||||
return <ApplyEffortAndClose result={result} onDone={onDone} />;
|
||||
|
||||
const result = executeEffort(args)
|
||||
return <ApplyEffortAndClose result={result} onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,32 +1,44 @@
|
||||
import { feature } from 'bun:bundle';
|
||||
import { spawnSync } from 'child_process';
|
||||
import sample from 'lodash-es/sample.js';
|
||||
import * as React from 'react';
|
||||
import { ExitFlow } from '../../components/ExitFlow.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { isBgSession } from '../../utils/concurrentSessions.js';
|
||||
import { gracefulShutdown } from '../../utils/gracefulShutdown.js';
|
||||
import { getCurrentWorktreeSession } from '../../utils/worktree.js';
|
||||
const GOODBYE_MESSAGES = ['Goodbye!', 'See ya!', 'Bye!', 'Catch you later!'];
|
||||
import { feature } from 'bun:bundle'
|
||||
import { spawnSync } from 'child_process'
|
||||
import sample from 'lodash-es/sample.js'
|
||||
import * as React from 'react'
|
||||
import { ExitFlow } from '../../components/ExitFlow.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import { isBgSession } from '../../utils/concurrentSessions.js'
|
||||
import { gracefulShutdown } from '../../utils/gracefulShutdown.js'
|
||||
import { getCurrentWorktreeSession } from '../../utils/worktree.js'
|
||||
|
||||
const GOODBYE_MESSAGES = ['Goodbye!', 'See ya!', 'Bye!', 'Catch you later!']
|
||||
|
||||
function getRandomGoodbyeMessage(): string {
|
||||
return sample(GOODBYE_MESSAGES) ?? 'Goodbye!';
|
||||
return sample(GOODBYE_MESSAGES) ?? 'Goodbye!'
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone): Promise<React.ReactNode> {
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
): Promise<React.ReactNode> {
|
||||
// Inside a `claude --bg` tmux session: detach instead of kill. The REPL
|
||||
// keeps running; `claude attach` can reconnect. Covers /exit, /quit,
|
||||
// ctrl+c, ctrl+d — all funnel through here via REPL's handleExit.
|
||||
if (feature('BG_SESSIONS') && isBgSession()) {
|
||||
onDone();
|
||||
spawnSync('tmux', ['detach-client'], {
|
||||
stdio: 'ignore'
|
||||
});
|
||||
return null;
|
||||
onDone()
|
||||
spawnSync('tmux', ['detach-client'], { stdio: 'ignore' })
|
||||
return null
|
||||
}
|
||||
const showWorktree = getCurrentWorktreeSession() !== null;
|
||||
|
||||
const showWorktree = getCurrentWorktreeSession() !== null
|
||||
|
||||
if (showWorktree) {
|
||||
return <ExitFlow showWorktree={showWorktree} onDone={onDone} onCancel={() => onDone()} />;
|
||||
return (
|
||||
<ExitFlow
|
||||
showWorktree={showWorktree}
|
||||
onDone={onDone}
|
||||
onCancel={() => onDone()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onDone(getRandomGoodbyeMessage());
|
||||
await gracefulShutdown(0, 'prompt_input_exit');
|
||||
return null;
|
||||
|
||||
onDone(getRandomGoodbyeMessage())
|
||||
await gracefulShutdown(0, 'prompt_input_exit')
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,90 +1,121 @@
|
||||
import { join } from 'path';
|
||||
import React from 'react';
|
||||
import { ExportDialog } from '../../components/ExportDialog.js';
|
||||
import type { ToolUseContext } from '../../Tool.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import type { Message } from '../../types/message.js';
|
||||
import { getCwd } from '../../utils/cwd.js';
|
||||
import { renderMessagesToPlainText } from '../../utils/exportRenderer.js';
|
||||
import { writeFileSync_DEPRECATED } from '../../utils/slowOperations.js';
|
||||
import { join } from 'path'
|
||||
import React from 'react'
|
||||
import { ExportDialog } from '../../components/ExportDialog.js'
|
||||
import type { ToolUseContext } from '../../Tool.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import type { Message } from '../../types/message.js'
|
||||
import { getCwd } from '../../utils/cwd.js'
|
||||
import { renderMessagesToPlainText } from '../../utils/exportRenderer.js'
|
||||
import { writeFileSync_DEPRECATED } from '../../utils/slowOperations.js'
|
||||
|
||||
function formatTimestamp(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}-${hours}${minutes}${seconds}`;
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}-${hours}${minutes}${seconds}`
|
||||
}
|
||||
|
||||
export function extractFirstPrompt(messages: Message[]): string {
|
||||
const firstUserMessage = messages.find(msg => msg.type === 'user');
|
||||
const firstUserMessage = messages.find(msg => msg.type === 'user')
|
||||
|
||||
if (!firstUserMessage || firstUserMessage.type !== 'user') {
|
||||
return '';
|
||||
return ''
|
||||
}
|
||||
const content = firstUserMessage.message?.content;
|
||||
let result = '';
|
||||
|
||||
const content = firstUserMessage.message?.content
|
||||
let result = ''
|
||||
|
||||
if (typeof content === 'string') {
|
||||
result = content.trim();
|
||||
result = content.trim()
|
||||
} else if (Array.isArray(content)) {
|
||||
const textContent = content.find(item => item.type === 'text');
|
||||
const textContent = content.find(item => item.type === 'text')
|
||||
if (textContent && 'text' in textContent) {
|
||||
result = textContent.text.trim();
|
||||
result = textContent.text.trim()
|
||||
}
|
||||
}
|
||||
|
||||
// Take first line only and limit length
|
||||
result = result.split('\n')[0] || '';
|
||||
result = result.split('\n')[0] || ''
|
||||
if (result.length > 50) {
|
||||
result = result.substring(0, 49) + '…';
|
||||
result = result.substring(0, 49) + '…'
|
||||
}
|
||||
return result;
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function sanitizeFilename(text: string): string {
|
||||
// Replace special characters with hyphens
|
||||
return text.toLowerCase().replace(/[^a-z0-9\s-]/g, '') // Remove special chars
|
||||
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||
.replace(/-+/g, '-') // Replace multiple hyphens with single
|
||||
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '') // Remove special chars
|
||||
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||
.replace(/-+/g, '-') // Replace multiple hyphens with single
|
||||
.replace(/^-|-$/g, '') // Remove leading/trailing hyphens
|
||||
}
|
||||
async function exportWithReactRenderer(context: ToolUseContext): Promise<string> {
|
||||
const tools = context.options.tools || [];
|
||||
return renderMessagesToPlainText(context.messages, tools);
|
||||
|
||||
async function exportWithReactRenderer(
|
||||
context: ToolUseContext,
|
||||
): Promise<string> {
|
||||
const tools = context.options.tools || []
|
||||
return renderMessagesToPlainText(context.messages, tools)
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: ToolUseContext, args: string): Promise<React.ReactNode> {
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: ToolUseContext,
|
||||
args: string,
|
||||
): Promise<React.ReactNode> {
|
||||
// Render the conversation content
|
||||
const content = await exportWithReactRenderer(context);
|
||||
const content = await exportWithReactRenderer(context)
|
||||
|
||||
// If args are provided, write directly to file and skip dialog
|
||||
const filename = args.trim();
|
||||
const filename = args.trim()
|
||||
if (filename) {
|
||||
const finalFilename = filename.endsWith('.txt') ? filename : filename.replace(/\.[^.]+$/, '') + '.txt';
|
||||
const filepath = join(getCwd(), finalFilename);
|
||||
const finalFilename = filename.endsWith('.txt')
|
||||
? filename
|
||||
: filename.replace(/\.[^.]+$/, '') + '.txt'
|
||||
const filepath = join(getCwd(), finalFilename)
|
||||
|
||||
try {
|
||||
writeFileSync_DEPRECATED(filepath, content, {
|
||||
encoding: 'utf-8',
|
||||
flush: true
|
||||
});
|
||||
onDone(`Conversation exported to: ${filepath}`);
|
||||
return null;
|
||||
flush: true,
|
||||
})
|
||||
onDone(`Conversation exported to: ${filepath}`)
|
||||
return null
|
||||
} catch (error) {
|
||||
onDone(`Failed to export conversation: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
return null;
|
||||
onDone(
|
||||
`Failed to export conversation: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Generate default filename from first prompt or timestamp
|
||||
const firstPrompt = extractFirstPrompt(context.messages);
|
||||
const timestamp = formatTimestamp(new Date());
|
||||
let defaultFilename: string;
|
||||
const firstPrompt = extractFirstPrompt(context.messages)
|
||||
const timestamp = formatTimestamp(new Date())
|
||||
|
||||
let defaultFilename: string
|
||||
if (firstPrompt) {
|
||||
const sanitized = sanitizeFilename(firstPrompt);
|
||||
defaultFilename = sanitized ? `${timestamp}-${sanitized}.txt` : `conversation-${timestamp}.txt`;
|
||||
const sanitized = sanitizeFilename(firstPrompt)
|
||||
defaultFilename = sanitized
|
||||
? `${timestamp}-${sanitized}.txt`
|
||||
: `conversation-${timestamp}.txt`
|
||||
} else {
|
||||
defaultFilename = `conversation-${timestamp}.txt`;
|
||||
defaultFilename = `conversation-${timestamp}.txt`
|
||||
}
|
||||
|
||||
// Return the dialog component when no args provided
|
||||
return <ExportDialog content={content} defaultFilename={defaultFilename} onDone={result => {
|
||||
onDone(result.message);
|
||||
}} />;
|
||||
return (
|
||||
<ExportDialog
|
||||
content={content}
|
||||
defaultFilename={defaultFilename}
|
||||
onDone={result => {
|
||||
onDone(result.message)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
import React from 'react';
|
||||
import type { LocalJSXCommandContext } from '../../commands.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { Login } from '../login/login.js';
|
||||
import { runExtraUsage } from './extra-usage-core.js';
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext): Promise<React.ReactNode | null> {
|
||||
const result = await runExtraUsage();
|
||||
import React from 'react'
|
||||
import type { LocalJSXCommandContext } from '../../commands.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import { Login } from '../login/login.js'
|
||||
import { runExtraUsage } from './extra-usage-core.js'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
): Promise<React.ReactNode | null> {
|
||||
const result = await runExtraUsage()
|
||||
|
||||
if (result.type === 'message') {
|
||||
onDone(result.value);
|
||||
return null;
|
||||
onDone(result.value)
|
||||
return null
|
||||
}
|
||||
return <Login startingMessage={'Starting new login following /extra-usage. Exit with Ctrl-C to use existing account.'} onDone={success => {
|
||||
context.onChangeAPIKey();
|
||||
onDone(success ? 'Login successful' : 'Login interrupted');
|
||||
}} />;
|
||||
|
||||
return (
|
||||
<Login
|
||||
startingMessage={
|
||||
'Starting new login following /extra-usage. Exit with Ctrl-C to use existing account.'
|
||||
}
|
||||
onDone={success => {
|
||||
context.onChangeAPIKey()
|
||||
onDone(success ? 'Login successful' : 'Login interrupted')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,268 +1,260 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import type { CommandResultDisplay, LocalJSXCommandContext } from '../../commands.js';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { FastIcon, getFastIconString } from '../../components/FastIcon.js';
|
||||
import { Box, Link, Text } from '../../ink.js';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js';
|
||||
import { type AppState, useAppState, useSetAppState } from '../../state/AppState.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { clearFastModeCooldown, FAST_MODE_MODEL_DISPLAY, getFastModeModel, getFastModeRuntimeState, getFastModeUnavailableReason, isFastModeEnabled, isFastModeSupportedByModel, prefetchFastModeStatus } from '../../utils/fastMode.js';
|
||||
import { formatDuration } from '../../utils/format.js';
|
||||
import { formatModelPricing, getOpus46CostTier } from '../../utils/modelCost.js';
|
||||
import { updateSettingsForSource } from '../../utils/settings/settings.js';
|
||||
function applyFastMode(enable: boolean, setAppState: (f: (prev: AppState) => AppState) => void): void {
|
||||
clearFastModeCooldown();
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import type {
|
||||
CommandResultDisplay,
|
||||
LocalJSXCommandContext,
|
||||
} from '../../commands.js'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { FastIcon, getFastIconString } from '../../components/FastIcon.js'
|
||||
import { Box, Link, Text } from '../../ink.js'
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
import {
|
||||
type AppState,
|
||||
useAppState,
|
||||
useSetAppState,
|
||||
} from '../../state/AppState.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import {
|
||||
clearFastModeCooldown,
|
||||
FAST_MODE_MODEL_DISPLAY,
|
||||
getFastModeModel,
|
||||
getFastModeRuntimeState,
|
||||
getFastModeUnavailableReason,
|
||||
isFastModeEnabled,
|
||||
isFastModeSupportedByModel,
|
||||
prefetchFastModeStatus,
|
||||
} from '../../utils/fastMode.js'
|
||||
import { formatDuration } from '../../utils/format.js'
|
||||
import { formatModelPricing, getOpus46CostTier } from '../../utils/modelCost.js'
|
||||
import { updateSettingsForSource } from '../../utils/settings/settings.js'
|
||||
|
||||
function applyFastMode(
|
||||
enable: boolean,
|
||||
setAppState: (f: (prev: AppState) => AppState) => void,
|
||||
): void {
|
||||
clearFastModeCooldown()
|
||||
updateSettingsForSource('userSettings', {
|
||||
fastMode: enable ? true : undefined
|
||||
});
|
||||
fastMode: enable ? true : undefined,
|
||||
})
|
||||
if (enable) {
|
||||
setAppState(prev => {
|
||||
// Only switch model if current model doesn't support fast mode
|
||||
const needsModelSwitch = !isFastModeSupportedByModel(prev.mainLoopModel);
|
||||
const needsModelSwitch = !isFastModeSupportedByModel(prev.mainLoopModel)
|
||||
return {
|
||||
...prev,
|
||||
...(needsModelSwitch ? {
|
||||
mainLoopModel: getFastModeModel(),
|
||||
mainLoopModelForSession: null
|
||||
} : {}),
|
||||
fastMode: true
|
||||
};
|
||||
});
|
||||
} else {
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
fastMode: false
|
||||
}));
|
||||
}
|
||||
}
|
||||
export function FastModePicker(t0) {
|
||||
const $ = _c(30);
|
||||
const {
|
||||
onDone,
|
||||
unavailableReason
|
||||
} = t0;
|
||||
const model = useAppState(_temp);
|
||||
const initialFastMode = useAppState(_temp2);
|
||||
const setAppState = useSetAppState();
|
||||
const [enableFastMode, setEnableFastMode] = useState(initialFastMode ?? false);
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = getFastModeRuntimeState();
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const runtimeState = t1;
|
||||
const isCooldown = runtimeState.status === "cooldown";
|
||||
const isUnavailable = unavailableReason !== null;
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = formatModelPricing(getOpus46CostTier(true));
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const pricing = t2;
|
||||
let t3;
|
||||
if ($[2] !== enableFastMode || $[3] !== isUnavailable || $[4] !== model || $[5] !== onDone || $[6] !== setAppState) {
|
||||
t3 = function handleConfirm() {
|
||||
if (isUnavailable) {
|
||||
return;
|
||||
...(needsModelSwitch
|
||||
? { mainLoopModel: getFastModeModel(), mainLoopModelForSession: null }
|
||||
: {}),
|
||||
fastMode: true,
|
||||
}
|
||||
applyFastMode(enableFastMode, setAppState);
|
||||
logEvent("tengu_fast_mode_toggled", {
|
||||
enabled: enableFastMode,
|
||||
source: "picker" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
if (enableFastMode) {
|
||||
const fastIcon = getFastIconString(enableFastMode);
|
||||
const modelUpdated = !isFastModeSupportedByModel(model) ? ` · model set to ${FAST_MODE_MODEL_DISPLAY}` : "";
|
||||
onDone(`${fastIcon} Fast mode ON${modelUpdated} · ${pricing}`);
|
||||
} else {
|
||||
setAppState(_temp3);
|
||||
onDone("Fast mode OFF");
|
||||
})
|
||||
} else {
|
||||
setAppState(prev => ({ ...prev, fastMode: false }))
|
||||
}
|
||||
}
|
||||
|
||||
export function FastModePicker({
|
||||
onDone,
|
||||
unavailableReason,
|
||||
}: {
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
unavailableReason: string | null
|
||||
}): React.ReactNode {
|
||||
const model = useAppState(s => s.mainLoopModel)
|
||||
const initialFastMode = useAppState(s => s.fastMode)
|
||||
const setAppState = useSetAppState()
|
||||
const [enableFastMode, setEnableFastMode] = useState(initialFastMode ?? false)
|
||||
const runtimeState = getFastModeRuntimeState()
|
||||
const isCooldown = runtimeState.status === 'cooldown'
|
||||
const isUnavailable = unavailableReason !== null
|
||||
const pricing = formatModelPricing(getOpus46CostTier(true))
|
||||
|
||||
function handleConfirm(): void {
|
||||
if (isUnavailable) return
|
||||
applyFastMode(enableFastMode, setAppState)
|
||||
logEvent('tengu_fast_mode_toggled', {
|
||||
enabled: enableFastMode,
|
||||
source:
|
||||
'picker' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
if (enableFastMode) {
|
||||
const fastIcon = getFastIconString(enableFastMode)
|
||||
const modelUpdated = !isFastModeSupportedByModel(model)
|
||||
? ` · model set to ${FAST_MODE_MODEL_DISPLAY}`
|
||||
: ''
|
||||
onDone(`${fastIcon} Fast mode ON${modelUpdated} · ${pricing}`)
|
||||
} else {
|
||||
setAppState(prev => ({ ...prev, fastMode: false }))
|
||||
onDone(`Fast mode OFF`)
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel(): void {
|
||||
if (isUnavailable) {
|
||||
// Ensure fast mode is off if the org has disabled it
|
||||
if (initialFastMode) {
|
||||
applyFastMode(false, setAppState)
|
||||
}
|
||||
};
|
||||
$[2] = enableFastMode;
|
||||
$[3] = isUnavailable;
|
||||
$[4] = model;
|
||||
$[5] = onDone;
|
||||
$[6] = setAppState;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
onDone('Fast mode OFF', { display: 'system' })
|
||||
return
|
||||
}
|
||||
const message = initialFastMode
|
||||
? `${getFastIconString()} Kept Fast mode ON`
|
||||
: `Kept Fast mode OFF`
|
||||
onDone(message, { display: 'system' })
|
||||
}
|
||||
const handleConfirm = t3;
|
||||
let t4;
|
||||
if ($[8] !== initialFastMode || $[9] !== isUnavailable || $[10] !== onDone || $[11] !== setAppState) {
|
||||
t4 = function handleCancel() {
|
||||
if (isUnavailable) {
|
||||
if (initialFastMode) {
|
||||
applyFastMode(false, setAppState);
|
||||
}
|
||||
onDone("Fast mode OFF", {
|
||||
display: "system"
|
||||
});
|
||||
return;
|
||||
|
||||
function handleToggle(): void {
|
||||
if (isUnavailable) return
|
||||
setEnableFastMode(prev => !prev)
|
||||
}
|
||||
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:yes': handleConfirm,
|
||||
'confirm:nextField': handleToggle,
|
||||
'confirm:next': handleToggle,
|
||||
'confirm:previous': handleToggle,
|
||||
'confirm:cycleMode': handleToggle,
|
||||
'confirm:toggle': handleToggle,
|
||||
},
|
||||
{ context: 'Confirmation' },
|
||||
)
|
||||
|
||||
const title = (
|
||||
<Text>
|
||||
<FastIcon cooldown={isCooldown} /> Fast mode (research preview)
|
||||
</Text>
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={title}
|
||||
subtitle={`High-speed mode for ${FAST_MODE_MODEL_DISPLAY}. Billed as extra usage at a premium rate. Separate rate limits apply.`}
|
||||
onCancel={handleCancel}
|
||||
color="fastMode"
|
||||
inputGuide={exitState =>
|
||||
exitState.pending ? (
|
||||
<Text>Press {exitState.keyName} again to exit</Text>
|
||||
) : isUnavailable ? (
|
||||
<Text>Esc to cancel</Text>
|
||||
) : (
|
||||
<Text>Tab to toggle · Enter to confirm · Esc to cancel</Text>
|
||||
)
|
||||
}
|
||||
const message = initialFastMode ? `${getFastIconString()} Kept Fast mode ON` : "Kept Fast mode OFF";
|
||||
onDone(message, {
|
||||
display: "system"
|
||||
});
|
||||
};
|
||||
$[8] = initialFastMode;
|
||||
$[9] = isUnavailable;
|
||||
$[10] = onDone;
|
||||
$[11] = setAppState;
|
||||
$[12] = t4;
|
||||
} else {
|
||||
t4 = $[12];
|
||||
}
|
||||
const handleCancel = t4;
|
||||
let t5;
|
||||
if ($[13] !== isUnavailable) {
|
||||
t5 = function handleToggle() {
|
||||
if (isUnavailable) {
|
||||
return;
|
||||
}
|
||||
setEnableFastMode(_temp4);
|
||||
};
|
||||
$[13] = isUnavailable;
|
||||
$[14] = t5;
|
||||
} else {
|
||||
t5 = $[14];
|
||||
}
|
||||
const handleToggle = t5;
|
||||
let t6;
|
||||
if ($[15] !== handleConfirm || $[16] !== handleToggle) {
|
||||
t6 = {
|
||||
"confirm:yes": handleConfirm,
|
||||
"confirm:nextField": handleToggle,
|
||||
"confirm:next": handleToggle,
|
||||
"confirm:previous": handleToggle,
|
||||
"confirm:cycleMode": handleToggle,
|
||||
"confirm:toggle": handleToggle
|
||||
};
|
||||
$[15] = handleConfirm;
|
||||
$[16] = handleToggle;
|
||||
$[17] = t6;
|
||||
} else {
|
||||
t6 = $[17];
|
||||
}
|
||||
let t7;
|
||||
if ($[18] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[18] = t7;
|
||||
} else {
|
||||
t7 = $[18];
|
||||
}
|
||||
useKeybindings(t6, t7);
|
||||
let t8;
|
||||
if ($[19] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = <Text><FastIcon cooldown={isCooldown} /> Fast mode (research preview)</Text>;
|
||||
$[19] = t8;
|
||||
} else {
|
||||
t8 = $[19];
|
||||
}
|
||||
const title = t8;
|
||||
let t9;
|
||||
if ($[20] !== isUnavailable) {
|
||||
t9 = exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : isUnavailable ? <Text>Esc to cancel</Text> : <Text>Tab to toggle · Enter to confirm · Esc to cancel</Text>;
|
||||
$[20] = isUnavailable;
|
||||
$[21] = t9;
|
||||
} else {
|
||||
t9 = $[21];
|
||||
}
|
||||
let t10;
|
||||
if ($[22] !== enableFastMode || $[23] !== unavailableReason) {
|
||||
t10 = unavailableReason ? <Box marginLeft={2}><Text color="error">{unavailableReason}</Text></Box> : <><Box flexDirection="column" gap={0} marginLeft={2}><Box flexDirection="row" gap={2}><Text bold={true}>Fast mode</Text><Text color={enableFastMode ? "fastMode" : undefined} bold={enableFastMode}>{enableFastMode ? "ON " : "OFF"}</Text><Text dimColor={true}>{pricing}</Text></Box></Box>{isCooldown && runtimeState.status === "cooldown" && <Box marginLeft={2}><Text color="warning">{runtimeState.reason === "overloaded" ? "Fast mode overloaded and is temporarily unavailable" : "You've hit your fast limit"}{" \xB7 resets in "}{formatDuration(runtimeState.resetAt - Date.now(), {
|
||||
hideTrailingZeros: true
|
||||
})}</Text></Box>}</>;
|
||||
$[22] = enableFastMode;
|
||||
$[23] = unavailableReason;
|
||||
$[24] = t10;
|
||||
} else {
|
||||
t10 = $[24];
|
||||
}
|
||||
let t11;
|
||||
if ($[25] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t11 = <Text dimColor={true}>Learn more:{" "}<Link url="https://code.claude.com/docs/en/fast-mode">https://code.claude.com/docs/en/fast-mode</Link></Text>;
|
||||
$[25] = t11;
|
||||
} else {
|
||||
t11 = $[25];
|
||||
}
|
||||
let t12;
|
||||
if ($[26] !== handleCancel || $[27] !== t10 || $[28] !== t9) {
|
||||
t12 = <Dialog title={title} subtitle={`High-speed mode for ${FAST_MODE_MODEL_DISPLAY}. Billed as extra usage at a premium rate. Separate rate limits apply.`} onCancel={handleCancel} color="fastMode" inputGuide={t9}>{t10}{t11}</Dialog>;
|
||||
$[26] = handleCancel;
|
||||
$[27] = t10;
|
||||
$[28] = t9;
|
||||
$[29] = t12;
|
||||
} else {
|
||||
t12 = $[29];
|
||||
}
|
||||
return t12;
|
||||
>
|
||||
{unavailableReason ? (
|
||||
<Box marginLeft={2}>
|
||||
<Text color="error">{unavailableReason}</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<Box flexDirection="column" gap={0} marginLeft={2}>
|
||||
<Box flexDirection="row" gap={2}>
|
||||
<Text bold>Fast mode</Text>
|
||||
<Text
|
||||
color={enableFastMode ? 'fastMode' : undefined}
|
||||
bold={enableFastMode}
|
||||
>
|
||||
{enableFastMode ? 'ON ' : 'OFF'}
|
||||
</Text>
|
||||
<Text dimColor>{pricing}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{isCooldown && runtimeState.status === 'cooldown' && (
|
||||
<Box marginLeft={2}>
|
||||
<Text color="warning">
|
||||
{runtimeState.reason === 'overloaded'
|
||||
? 'Fast mode overloaded and is temporarily unavailable'
|
||||
: "You've hit your fast limit"}
|
||||
{' · resets in '}
|
||||
{formatDuration(runtimeState.resetAt - Date.now(), {
|
||||
hideTrailingZeros: true,
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Text dimColor>
|
||||
Learn more:{' '}
|
||||
<Link url="https://code.claude.com/docs/en/fast-mode">
|
||||
https://code.claude.com/docs/en/fast-mode
|
||||
</Link>
|
||||
</Text>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
function _temp4(prev_0) {
|
||||
return !prev_0;
|
||||
}
|
||||
function _temp3(prev) {
|
||||
return {
|
||||
...prev,
|
||||
fastMode: false
|
||||
};
|
||||
}
|
||||
function _temp2(s_0) {
|
||||
return s_0.fastMode;
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.mainLoopModel;
|
||||
}
|
||||
async function handleFastModeShortcut(enable: boolean, getAppState: () => AppState, setAppState: (f: (prev: AppState) => AppState) => void): Promise<string> {
|
||||
const unavailableReason = getFastModeUnavailableReason();
|
||||
|
||||
async function handleFastModeShortcut(
|
||||
enable: boolean,
|
||||
getAppState: () => AppState,
|
||||
setAppState: (f: (prev: AppState) => AppState) => void,
|
||||
): Promise<string> {
|
||||
const unavailableReason = getFastModeUnavailableReason()
|
||||
if (unavailableReason) {
|
||||
return `Fast mode unavailable: ${unavailableReason}`;
|
||||
return `Fast mode unavailable: ${unavailableReason}`
|
||||
}
|
||||
const {
|
||||
mainLoopModel
|
||||
} = getAppState();
|
||||
applyFastMode(enable, setAppState);
|
||||
|
||||
const { mainLoopModel } = getAppState()
|
||||
applyFastMode(enable, setAppState)
|
||||
logEvent('tengu_fast_mode_toggled', {
|
||||
enabled: enable,
|
||||
source: 'shortcut' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
source:
|
||||
'shortcut' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
|
||||
if (enable) {
|
||||
const fastIcon = getFastIconString(true);
|
||||
const modelUpdated = !isFastModeSupportedByModel(mainLoopModel) ? ` · model set to ${FAST_MODE_MODEL_DISPLAY}` : '';
|
||||
const pricing = formatModelPricing(getOpus46CostTier(true));
|
||||
return `${fastIcon} Fast mode ON${modelUpdated} · ${pricing}`;
|
||||
const fastIcon = getFastIconString(true)
|
||||
const modelUpdated = !isFastModeSupportedByModel(mainLoopModel)
|
||||
? ` · model set to ${FAST_MODE_MODEL_DISPLAY}`
|
||||
: ''
|
||||
const pricing = formatModelPricing(getOpus46CostTier(true))
|
||||
return `${fastIcon} Fast mode ON${modelUpdated} · ${pricing}`
|
||||
} else {
|
||||
return `Fast mode OFF`;
|
||||
return `Fast mode OFF`
|
||||
}
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext, args?: string): Promise<React.ReactNode | null> {
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
args?: string,
|
||||
): Promise<React.ReactNode | null> {
|
||||
if (!isFastModeEnabled()) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Fetch org fast mode status before showing the picker. We must know
|
||||
// whether the org has disabled fast mode before allowing any toggle.
|
||||
// If a startup prefetch is already in flight, this awaits it.
|
||||
await prefetchFastModeStatus();
|
||||
const arg = args?.trim().toLowerCase();
|
||||
await prefetchFastModeStatus()
|
||||
|
||||
const arg = args?.trim().toLowerCase()
|
||||
if (arg === 'on' || arg === 'off') {
|
||||
const result = await handleFastModeShortcut(arg === 'on', context.getAppState, context.setAppState);
|
||||
onDone(result);
|
||||
return null;
|
||||
const result = await handleFastModeShortcut(
|
||||
arg === 'on',
|
||||
context.getAppState,
|
||||
context.setAppState,
|
||||
)
|
||||
onDone(result)
|
||||
return null
|
||||
}
|
||||
const unavailableReason = getFastModeUnavailableReason();
|
||||
|
||||
const unavailableReason = getFastModeUnavailableReason()
|
||||
logEvent('tengu_fast_mode_picker_shown', {
|
||||
unavailable_reason: (unavailableReason ?? '') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
return <FastModePicker onDone={onDone} unavailableReason={unavailableReason} />;
|
||||
unavailable_reason: (unavailableReason ??
|
||||
'') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
return (
|
||||
<FastModePicker onDone={onDone} unavailableReason={unavailableReason} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,50 @@
|
||||
import * as React from 'react';
|
||||
import type { CommandResultDisplay, LocalJSXCommandContext } from '../../commands.js';
|
||||
import { Feedback } from '../../components/Feedback.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import type { Message } from '../../types/message.js';
|
||||
import * as React from 'react'
|
||||
import type {
|
||||
CommandResultDisplay,
|
||||
LocalJSXCommandContext,
|
||||
} from '../../commands.js'
|
||||
import { Feedback } from '../../components/Feedback.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import type { Message } from '../../types/message.js'
|
||||
|
||||
// Shared function to render the Feedback component
|
||||
export function renderFeedbackComponent(onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void, abortSignal: AbortSignal, messages: Message[], initialDescription: string = '', backgroundTasks: {
|
||||
[taskId: string]: {
|
||||
type: string;
|
||||
identity?: {
|
||||
agentId: string;
|
||||
};
|
||||
messages?: Message[];
|
||||
};
|
||||
} = {}): React.ReactNode {
|
||||
return <Feedback abortSignal={abortSignal} messages={messages} initialDescription={initialDescription} onDone={onDone} backgroundTasks={backgroundTasks} />;
|
||||
export function renderFeedbackComponent(
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void,
|
||||
abortSignal: AbortSignal,
|
||||
messages: Message[],
|
||||
initialDescription: string = '',
|
||||
backgroundTasks: {
|
||||
[taskId: string]: {
|
||||
type: string
|
||||
identity?: { agentId: string }
|
||||
messages?: Message[]
|
||||
}
|
||||
} = {},
|
||||
): React.ReactNode {
|
||||
return (
|
||||
<Feedback
|
||||
abortSignal={abortSignal}
|
||||
messages={messages}
|
||||
initialDescription={initialDescription}
|
||||
onDone={onDone}
|
||||
backgroundTasks={backgroundTasks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext, args?: string): Promise<React.ReactNode> {
|
||||
const initialDescription = args || '';
|
||||
return renderFeedbackComponent(onDone, context.abortController.signal, context.messages, initialDescription);
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
args?: string,
|
||||
): Promise<React.ReactNode> {
|
||||
const initialDescription = args || ''
|
||||
return renderFeedbackComponent(
|
||||
onDone,
|
||||
context.abortController.signal,
|
||||
context.messages,
|
||||
initialDescription,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { HelpV2 } from '../../components/HelpV2/HelpV2.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
export const call: LocalJSXCommandCall = async (onDone, {
|
||||
options: {
|
||||
commands
|
||||
}
|
||||
}) => {
|
||||
return <HelpV2 commands={commands} onClose={onDone} />;
|
||||
};
|
||||
import * as React from 'react'
|
||||
import { HelpV2 } from '../../components/HelpV2/HelpV2.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
|
||||
export const call: LocalJSXCommandCall = async (
|
||||
onDone,
|
||||
{ options: { commands } },
|
||||
) => {
|
||||
return <HelpV2 commands={commands} onClose={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import { HooksConfigMenu } from '../../components/hooks/HooksConfigMenu.js';
|
||||
import { logEvent } from '../../services/analytics/index.js';
|
||||
import { getTools } from '../../tools.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import * as React from 'react'
|
||||
import { HooksConfigMenu } from '../../components/hooks/HooksConfigMenu.js'
|
||||
import { logEvent } from '../../services/analytics/index.js'
|
||||
import { getTools } from '../../tools.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, context) => {
|
||||
logEvent('tengu_hooks_command', {});
|
||||
const appState = context.getAppState();
|
||||
const permissionContext = appState.toolPermissionContext;
|
||||
const toolNames = getTools(permissionContext).map(tool => tool.name);
|
||||
return <HooksConfigMenu toolNames={toolNames} onExit={onDone} />;
|
||||
};
|
||||
logEvent('tengu_hooks_command', {})
|
||||
const appState = context.getAppState()
|
||||
const permissionContext = appState.toolPermissionContext
|
||||
const toolNames = getTools(permissionContext).map(tool => tool.name)
|
||||
return <HooksConfigMenu toolNames={toolNames} onExit={onDone} />
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,230 +1,152 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import TextInput from '../../components/TextInput.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { Box, color, Text, useTheme } from '../../ink.js';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import TextInput from '../../components/TextInput.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import { Box, color, Text, useTheme } from '../../ink.js'
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||
|
||||
interface ApiKeyStepProps {
|
||||
existingApiKey: string | null;
|
||||
useExistingKey: boolean;
|
||||
apiKeyOrOAuthToken: string;
|
||||
onApiKeyChange: (value: string) => void;
|
||||
onToggleUseExistingKey: (useExisting: boolean) => void;
|
||||
onSubmit: () => void;
|
||||
onCreateOAuthToken?: () => void;
|
||||
selectedOption?: 'existing' | 'new' | 'oauth';
|
||||
onSelectOption?: (option: 'existing' | 'new' | 'oauth') => void;
|
||||
existingApiKey: string | null
|
||||
useExistingKey: boolean
|
||||
apiKeyOrOAuthToken: string
|
||||
onApiKeyChange: (value: string) => void
|
||||
onToggleUseExistingKey: (useExisting: boolean) => void
|
||||
onSubmit: () => void
|
||||
onCreateOAuthToken?: () => void
|
||||
selectedOption?: 'existing' | 'new' | 'oauth'
|
||||
onSelectOption?: (option: 'existing' | 'new' | 'oauth') => void
|
||||
}
|
||||
export function ApiKeyStep(t0) {
|
||||
const $ = _c(55);
|
||||
const {
|
||||
existingApiKey,
|
||||
apiKeyOrOAuthToken,
|
||||
onApiKeyChange,
|
||||
onSubmit,
|
||||
onToggleUseExistingKey,
|
||||
|
||||
export function ApiKeyStep({
|
||||
existingApiKey,
|
||||
apiKeyOrOAuthToken,
|
||||
onApiKeyChange,
|
||||
onSubmit,
|
||||
onToggleUseExistingKey,
|
||||
onCreateOAuthToken,
|
||||
selectedOption = existingApiKey
|
||||
? 'existing'
|
||||
: onCreateOAuthToken
|
||||
? 'oauth'
|
||||
: 'new',
|
||||
onSelectOption,
|
||||
}: ApiKeyStepProps) {
|
||||
const [cursorOffset, setCursorOffset] = useState(0)
|
||||
const terminalSize = useTerminalSize()
|
||||
const [theme] = useTheme()
|
||||
|
||||
const handlePrevious = useCallback(() => {
|
||||
if (selectedOption === 'new' && onCreateOAuthToken) {
|
||||
// From 'new' go up to 'oauth'
|
||||
onSelectOption?.('oauth')
|
||||
} else if (selectedOption === 'oauth' && existingApiKey) {
|
||||
// From 'oauth' go up to 'existing' (only if it exists)
|
||||
onSelectOption?.('existing')
|
||||
onToggleUseExistingKey(true)
|
||||
}
|
||||
}, [
|
||||
selectedOption,
|
||||
onCreateOAuthToken,
|
||||
selectedOption: t1,
|
||||
onSelectOption
|
||||
} = t0;
|
||||
const selectedOption = t1 === undefined ? existingApiKey ? "existing" : onCreateOAuthToken ? "oauth" : "new" : t1;
|
||||
const [cursorOffset, setCursorOffset] = useState(0);
|
||||
const terminalSize = useTerminalSize();
|
||||
const [theme] = useTheme();
|
||||
let t2;
|
||||
if ($[0] !== existingApiKey || $[1] !== onCreateOAuthToken || $[2] !== onSelectOption || $[3] !== onToggleUseExistingKey || $[4] !== selectedOption) {
|
||||
t2 = () => {
|
||||
if (selectedOption === "new" && onCreateOAuthToken) {
|
||||
onSelectOption?.("oauth");
|
||||
} else {
|
||||
if (selectedOption === "oauth" && existingApiKey) {
|
||||
onSelectOption?.("existing");
|
||||
onToggleUseExistingKey(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
$[0] = existingApiKey;
|
||||
$[1] = onCreateOAuthToken;
|
||||
$[2] = onSelectOption;
|
||||
$[3] = onToggleUseExistingKey;
|
||||
$[4] = selectedOption;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
const handlePrevious = t2;
|
||||
let t3;
|
||||
if ($[6] !== onCreateOAuthToken || $[7] !== onSelectOption || $[8] !== onToggleUseExistingKey || $[9] !== selectedOption) {
|
||||
t3 = () => {
|
||||
if (selectedOption === "existing") {
|
||||
onSelectOption?.(onCreateOAuthToken ? "oauth" : "new");
|
||||
onToggleUseExistingKey(false);
|
||||
} else {
|
||||
if (selectedOption === "oauth") {
|
||||
onSelectOption?.("new");
|
||||
}
|
||||
}
|
||||
};
|
||||
$[6] = onCreateOAuthToken;
|
||||
$[7] = onSelectOption;
|
||||
$[8] = onToggleUseExistingKey;
|
||||
$[9] = selectedOption;
|
||||
$[10] = t3;
|
||||
} else {
|
||||
t3 = $[10];
|
||||
}
|
||||
const handleNext = t3;
|
||||
let t4;
|
||||
if ($[11] !== onCreateOAuthToken || $[12] !== onSubmit || $[13] !== selectedOption) {
|
||||
t4 = () => {
|
||||
if (selectedOption === "oauth" && onCreateOAuthToken) {
|
||||
onCreateOAuthToken();
|
||||
} else {
|
||||
onSubmit();
|
||||
}
|
||||
};
|
||||
$[11] = onCreateOAuthToken;
|
||||
$[12] = onSubmit;
|
||||
$[13] = selectedOption;
|
||||
$[14] = t4;
|
||||
} else {
|
||||
t4 = $[14];
|
||||
}
|
||||
const handleConfirm = t4;
|
||||
const isTextInputVisible = selectedOption === "new";
|
||||
let t5;
|
||||
if ($[15] !== handleConfirm || $[16] !== handleNext || $[17] !== handlePrevious) {
|
||||
t5 = {
|
||||
"confirm:previous": handlePrevious,
|
||||
"confirm:next": handleNext,
|
||||
"confirm:yes": handleConfirm
|
||||
};
|
||||
$[15] = handleConfirm;
|
||||
$[16] = handleNext;
|
||||
$[17] = handlePrevious;
|
||||
$[18] = t5;
|
||||
} else {
|
||||
t5 = $[18];
|
||||
}
|
||||
const t6 = !isTextInputVisible;
|
||||
let t7;
|
||||
if ($[19] !== t6) {
|
||||
t7 = {
|
||||
context: "Confirmation",
|
||||
isActive: t6
|
||||
};
|
||||
$[19] = t6;
|
||||
$[20] = t7;
|
||||
} else {
|
||||
t7 = $[20];
|
||||
}
|
||||
useKeybindings(t5, t7);
|
||||
let t8;
|
||||
if ($[21] !== handleNext || $[22] !== handlePrevious) {
|
||||
t8 = {
|
||||
"confirm:previous": handlePrevious,
|
||||
"confirm:next": handleNext
|
||||
};
|
||||
$[21] = handleNext;
|
||||
$[22] = handlePrevious;
|
||||
$[23] = t8;
|
||||
} else {
|
||||
t8 = $[23];
|
||||
}
|
||||
let t9;
|
||||
if ($[24] !== isTextInputVisible) {
|
||||
t9 = {
|
||||
context: "Confirmation",
|
||||
isActive: isTextInputVisible
|
||||
};
|
||||
$[24] = isTextInputVisible;
|
||||
$[25] = t9;
|
||||
} else {
|
||||
t9 = $[25];
|
||||
}
|
||||
useKeybindings(t8, t9);
|
||||
let t10;
|
||||
if ($[26] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t10 = <Box flexDirection="column" marginBottom={1}><Text bold={true}>Install GitHub App</Text><Text dimColor={true}>Choose API key</Text></Box>;
|
||||
$[26] = t10;
|
||||
} else {
|
||||
t10 = $[26];
|
||||
}
|
||||
let t11;
|
||||
if ($[27] !== existingApiKey || $[28] !== selectedOption || $[29] !== theme) {
|
||||
t11 = existingApiKey && <Box marginBottom={1}><Text>{selectedOption === "existing" ? color("success", theme)("> ") : " "}Use your existing Claude Code API key</Text></Box>;
|
||||
$[27] = existingApiKey;
|
||||
$[28] = selectedOption;
|
||||
$[29] = theme;
|
||||
$[30] = t11;
|
||||
} else {
|
||||
t11 = $[30];
|
||||
}
|
||||
let t12;
|
||||
if ($[31] !== onCreateOAuthToken || $[32] !== selectedOption || $[33] !== theme) {
|
||||
t12 = onCreateOAuthToken && <Box marginBottom={1}><Text>{selectedOption === "oauth" ? color("success", theme)("> ") : " "}Create a long-lived token with your Claude subscription</Text></Box>;
|
||||
$[31] = onCreateOAuthToken;
|
||||
$[32] = selectedOption;
|
||||
$[33] = theme;
|
||||
$[34] = t12;
|
||||
} else {
|
||||
t12 = $[34];
|
||||
}
|
||||
let t13;
|
||||
if ($[35] !== selectedOption || $[36] !== theme) {
|
||||
t13 = selectedOption === "new" ? color("success", theme)("> ") : " ";
|
||||
$[35] = selectedOption;
|
||||
$[36] = theme;
|
||||
$[37] = t13;
|
||||
} else {
|
||||
t13 = $[37];
|
||||
}
|
||||
let t14;
|
||||
if ($[38] !== t13) {
|
||||
t14 = <Box marginBottom={1}><Text>{t13}Enter a new API key</Text></Box>;
|
||||
$[38] = t13;
|
||||
$[39] = t14;
|
||||
} else {
|
||||
t14 = $[39];
|
||||
}
|
||||
let t15;
|
||||
if ($[40] !== apiKeyOrOAuthToken || $[41] !== cursorOffset || $[42] !== onApiKeyChange || $[43] !== onSubmit || $[44] !== selectedOption || $[45] !== terminalSize) {
|
||||
t15 = selectedOption === "new" && <TextInput value={apiKeyOrOAuthToken} onChange={onApiKeyChange} onSubmit={onSubmit} onPaste={onApiKeyChange} focus={true} placeholder={"sk-ant\u2026 (Create a new key at https://platform.claude.com/settings/keys)"} mask="*" columns={terminalSize.columns} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} showCursor={true} />;
|
||||
$[40] = apiKeyOrOAuthToken;
|
||||
$[41] = cursorOffset;
|
||||
$[42] = onApiKeyChange;
|
||||
$[43] = onSubmit;
|
||||
$[44] = selectedOption;
|
||||
$[45] = terminalSize;
|
||||
$[46] = t15;
|
||||
} else {
|
||||
t15 = $[46];
|
||||
}
|
||||
let t16;
|
||||
if ($[47] !== t11 || $[48] !== t12 || $[49] !== t14 || $[50] !== t15) {
|
||||
t16 = <Box flexDirection="column" borderStyle="round" paddingX={1}>{t10}{t11}{t12}{t14}{t15}</Box>;
|
||||
$[47] = t11;
|
||||
$[48] = t12;
|
||||
$[49] = t14;
|
||||
$[50] = t15;
|
||||
$[51] = t16;
|
||||
} else {
|
||||
t16 = $[51];
|
||||
}
|
||||
let t17;
|
||||
if ($[52] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t17 = <Box marginLeft={3}><Text dimColor={true}>↑/↓ to select · Enter to continue</Text></Box>;
|
||||
$[52] = t17;
|
||||
} else {
|
||||
t17 = $[52];
|
||||
}
|
||||
let t18;
|
||||
if ($[53] !== t16) {
|
||||
t18 = <>{t16}{t17}</>;
|
||||
$[53] = t16;
|
||||
$[54] = t18;
|
||||
} else {
|
||||
t18 = $[54];
|
||||
}
|
||||
return t18;
|
||||
existingApiKey,
|
||||
onSelectOption,
|
||||
onToggleUseExistingKey,
|
||||
])
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
if (selectedOption === 'existing') {
|
||||
// From 'existing' go down to 'oauth' (if available) or 'new'
|
||||
onSelectOption?.(onCreateOAuthToken ? 'oauth' : 'new')
|
||||
onToggleUseExistingKey(false)
|
||||
} else if (selectedOption === 'oauth') {
|
||||
// From 'oauth' go down to 'new'
|
||||
onSelectOption?.('new')
|
||||
}
|
||||
}, [
|
||||
selectedOption,
|
||||
onCreateOAuthToken,
|
||||
onSelectOption,
|
||||
onToggleUseExistingKey,
|
||||
])
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
if (selectedOption === 'oauth' && onCreateOAuthToken) {
|
||||
onCreateOAuthToken()
|
||||
} else {
|
||||
onSubmit()
|
||||
}
|
||||
}, [selectedOption, onCreateOAuthToken, onSubmit])
|
||||
|
||||
// When the text input is visible, omit confirm:yes so bare 'y' passes
|
||||
// through to the input instead of submitting. TextInput's onSubmit handles
|
||||
// Enter. Keep the Confirmation context (not Settings) to avoid j/k bindings.
|
||||
const isTextInputVisible = selectedOption === 'new'
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:previous': handlePrevious,
|
||||
'confirm:next': handleNext,
|
||||
'confirm:yes': handleConfirm,
|
||||
},
|
||||
{ context: 'Confirmation', isActive: !isTextInputVisible },
|
||||
)
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:previous': handlePrevious,
|
||||
'confirm:next': handleNext,
|
||||
},
|
||||
{ context: 'Confirmation', isActive: isTextInputVisible },
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box flexDirection="column" borderStyle="round" paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>Install GitHub App</Text>
|
||||
<Text dimColor>Choose API key</Text>
|
||||
</Box>
|
||||
{existingApiKey && (
|
||||
<Box marginBottom={1}>
|
||||
<Text>
|
||||
{selectedOption === 'existing'
|
||||
? color('success', theme)('> ')
|
||||
: ' '}
|
||||
Use your existing Claude Code API key
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{onCreateOAuthToken && (
|
||||
<Box marginBottom={1}>
|
||||
<Text>
|
||||
{selectedOption === 'oauth'
|
||||
? color('success', theme)('> ')
|
||||
: ' '}
|
||||
Create a long-lived token with your Claude subscription
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box marginBottom={1}>
|
||||
<Text>
|
||||
{selectedOption === 'new' ? color('success', theme)('> ') : ' '}
|
||||
Enter a new API key
|
||||
</Text>
|
||||
</Box>
|
||||
{selectedOption === 'new' && (
|
||||
<TextInput
|
||||
value={apiKeyOrOAuthToken}
|
||||
onChange={onApiKeyChange}
|
||||
onSubmit={onSubmit}
|
||||
onPaste={onApiKeyChange}
|
||||
focus={true}
|
||||
placeholder="sk-ant… (Create a new key at https://platform.claude.com/settings/keys)"
|
||||
mask="*"
|
||||
columns={terminalSize.columns}
|
||||
cursorOffset={cursorOffset}
|
||||
onChangeCursorOffset={setCursorOffset}
|
||||
showCursor={true}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box marginLeft={3}>
|
||||
<Text dimColor>↑/↓ to select · Enter to continue</Text>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,189 +1,106 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import TextInput from '../../components/TextInput.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { Box, color, Text, useTheme } from '../../ink.js';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import TextInput from '../../components/TextInput.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import { Box, color, Text, useTheme } from '../../ink.js'
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||
|
||||
interface CheckExistingSecretStepProps {
|
||||
useExistingSecret: boolean;
|
||||
secretName: string;
|
||||
onToggleUseExistingSecret: (useExisting: boolean) => void;
|
||||
onSecretNameChange: (value: string) => void;
|
||||
onSubmit: () => void;
|
||||
useExistingSecret: boolean
|
||||
secretName: string
|
||||
onToggleUseExistingSecret: (useExisting: boolean) => void
|
||||
onSecretNameChange: (value: string) => void
|
||||
onSubmit: () => void
|
||||
}
|
||||
export function CheckExistingSecretStep(t0) {
|
||||
const $ = _c(42);
|
||||
const {
|
||||
useExistingSecret,
|
||||
secretName,
|
||||
onToggleUseExistingSecret,
|
||||
onSecretNameChange,
|
||||
onSubmit
|
||||
} = t0;
|
||||
const [cursorOffset, setCursorOffset] = useState(0);
|
||||
const terminalSize = useTerminalSize();
|
||||
const [theme] = useTheme();
|
||||
let t1;
|
||||
if ($[0] !== onToggleUseExistingSecret) {
|
||||
t1 = () => onToggleUseExistingSecret(true);
|
||||
$[0] = onToggleUseExistingSecret;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const handlePrevious = t1;
|
||||
let t2;
|
||||
if ($[2] !== onToggleUseExistingSecret) {
|
||||
t2 = () => onToggleUseExistingSecret(false);
|
||||
$[2] = onToggleUseExistingSecret;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const handleNext = t2;
|
||||
let t3;
|
||||
if ($[4] !== handleNext || $[5] !== handlePrevious || $[6] !== onSubmit) {
|
||||
t3 = {
|
||||
"confirm:previous": handlePrevious,
|
||||
"confirm:next": handleNext,
|
||||
"confirm:yes": onSubmit
|
||||
};
|
||||
$[4] = handleNext;
|
||||
$[5] = handlePrevious;
|
||||
$[6] = onSubmit;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
let t4;
|
||||
if ($[8] !== useExistingSecret) {
|
||||
t4 = {
|
||||
context: "Confirmation",
|
||||
isActive: useExistingSecret
|
||||
};
|
||||
$[8] = useExistingSecret;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
useKeybindings(t3, t4);
|
||||
let t5;
|
||||
if ($[10] !== handleNext || $[11] !== handlePrevious) {
|
||||
t5 = {
|
||||
"confirm:previous": handlePrevious,
|
||||
"confirm:next": handleNext
|
||||
};
|
||||
$[10] = handleNext;
|
||||
$[11] = handlePrevious;
|
||||
$[12] = t5;
|
||||
} else {
|
||||
t5 = $[12];
|
||||
}
|
||||
const t6 = !useExistingSecret;
|
||||
let t7;
|
||||
if ($[13] !== t6) {
|
||||
t7 = {
|
||||
context: "Confirmation",
|
||||
isActive: t6
|
||||
};
|
||||
$[13] = t6;
|
||||
$[14] = t7;
|
||||
} else {
|
||||
t7 = $[14];
|
||||
}
|
||||
useKeybindings(t5, t7);
|
||||
let t8;
|
||||
if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = <Box flexDirection="column" marginBottom={1}><Text bold={true}>Install GitHub App</Text><Text dimColor={true}>Setup API key secret</Text></Box>;
|
||||
$[15] = t8;
|
||||
} else {
|
||||
t8 = $[15];
|
||||
}
|
||||
let t9;
|
||||
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = <Box marginBottom={1}><Text color="warning">ANTHROPIC_API_KEY already exists in repository secrets!</Text></Box>;
|
||||
$[16] = t9;
|
||||
} else {
|
||||
t9 = $[16];
|
||||
}
|
||||
let t10;
|
||||
if ($[17] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t10 = <Box marginBottom={1}><Text>Would you like to:</Text></Box>;
|
||||
$[17] = t10;
|
||||
} else {
|
||||
t10 = $[17];
|
||||
}
|
||||
let t11;
|
||||
if ($[18] !== theme || $[19] !== useExistingSecret) {
|
||||
t11 = useExistingSecret ? color("success", theme)("> ") : " ";
|
||||
$[18] = theme;
|
||||
$[19] = useExistingSecret;
|
||||
$[20] = t11;
|
||||
} else {
|
||||
t11 = $[20];
|
||||
}
|
||||
let t12;
|
||||
if ($[21] !== t11) {
|
||||
t12 = <Box marginBottom={1}><Text>{t11}Use the existing API key</Text></Box>;
|
||||
$[21] = t11;
|
||||
$[22] = t12;
|
||||
} else {
|
||||
t12 = $[22];
|
||||
}
|
||||
let t13;
|
||||
if ($[23] !== theme || $[24] !== useExistingSecret) {
|
||||
t13 = !useExistingSecret ? color("success", theme)("> ") : " ";
|
||||
$[23] = theme;
|
||||
$[24] = useExistingSecret;
|
||||
$[25] = t13;
|
||||
} else {
|
||||
t13 = $[25];
|
||||
}
|
||||
let t14;
|
||||
if ($[26] !== t13) {
|
||||
t14 = <Box marginBottom={1}><Text>{t13}Create a new secret with a different name</Text></Box>;
|
||||
$[26] = t13;
|
||||
$[27] = t14;
|
||||
} else {
|
||||
t14 = $[27];
|
||||
}
|
||||
let t15;
|
||||
if ($[28] !== cursorOffset || $[29] !== onSecretNameChange || $[30] !== onSubmit || $[31] !== secretName || $[32] !== terminalSize || $[33] !== useExistingSecret) {
|
||||
t15 = !useExistingSecret && <><Box marginBottom={1}><Text>Enter new secret name (alphanumeric with underscores):</Text></Box><TextInput value={secretName} onChange={onSecretNameChange} onSubmit={onSubmit} focus={true} placeholder="e.g., CLAUDE_API_KEY" columns={terminalSize.columns} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} showCursor={true} /></>;
|
||||
$[28] = cursorOffset;
|
||||
$[29] = onSecretNameChange;
|
||||
$[30] = onSubmit;
|
||||
$[31] = secretName;
|
||||
$[32] = terminalSize;
|
||||
$[33] = useExistingSecret;
|
||||
$[34] = t15;
|
||||
} else {
|
||||
t15 = $[34];
|
||||
}
|
||||
let t16;
|
||||
if ($[35] !== t12 || $[36] !== t14 || $[37] !== t15) {
|
||||
t16 = <Box flexDirection="column" borderStyle="round" paddingX={1}>{t8}{t9}{t10}{t12}{t14}{t15}</Box>;
|
||||
$[35] = t12;
|
||||
$[36] = t14;
|
||||
$[37] = t15;
|
||||
$[38] = t16;
|
||||
} else {
|
||||
t16 = $[38];
|
||||
}
|
||||
let t17;
|
||||
if ($[39] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t17 = <Box marginLeft={3}><Text dimColor={true}>↑/↓ to select · Enter to continue</Text></Box>;
|
||||
$[39] = t17;
|
||||
} else {
|
||||
t17 = $[39];
|
||||
}
|
||||
let t18;
|
||||
if ($[40] !== t16) {
|
||||
t18 = <>{t16}{t17}</>;
|
||||
$[40] = t16;
|
||||
$[41] = t18;
|
||||
} else {
|
||||
t18 = $[41];
|
||||
}
|
||||
return t18;
|
||||
|
||||
export function CheckExistingSecretStep({
|
||||
useExistingSecret,
|
||||
secretName,
|
||||
onToggleUseExistingSecret,
|
||||
onSecretNameChange,
|
||||
onSubmit,
|
||||
}: CheckExistingSecretStepProps) {
|
||||
const [cursorOffset, setCursorOffset] = useState(0)
|
||||
const terminalSize = useTerminalSize()
|
||||
const [theme] = useTheme()
|
||||
|
||||
// When the text input is visible, omit confirm:yes so bare 'y' passes
|
||||
// through to the input instead of submitting. TextInput's onSubmit handles
|
||||
// Enter. Keep the Confirmation context (not Settings) to avoid j/k bindings.
|
||||
const handlePrevious = useCallback(
|
||||
() => onToggleUseExistingSecret(true),
|
||||
[onToggleUseExistingSecret],
|
||||
)
|
||||
const handleNext = useCallback(
|
||||
() => onToggleUseExistingSecret(false),
|
||||
[onToggleUseExistingSecret],
|
||||
)
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:previous': handlePrevious,
|
||||
'confirm:next': handleNext,
|
||||
'confirm:yes': onSubmit,
|
||||
},
|
||||
{ context: 'Confirmation', isActive: useExistingSecret },
|
||||
)
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:previous': handlePrevious,
|
||||
'confirm:next': handleNext,
|
||||
},
|
||||
{ context: 'Confirmation', isActive: !useExistingSecret },
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box flexDirection="column" borderStyle="round" paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>Install GitHub App</Text>
|
||||
<Text dimColor>Setup API key secret</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>
|
||||
<Text color="warning">
|
||||
ANTHROPIC_API_KEY already exists in repository secrets!
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>
|
||||
<Text>Would you like to:</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>
|
||||
<Text>
|
||||
{useExistingSecret ? color('success', theme)('> ') : ' '}
|
||||
Use the existing API key
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>
|
||||
<Text>
|
||||
{!useExistingSecret ? color('success', theme)('> ') : ' '}
|
||||
Create a new secret with a different name
|
||||
</Text>
|
||||
</Box>
|
||||
{!useExistingSecret && (
|
||||
<>
|
||||
<Box marginBottom={1}>
|
||||
<Text>
|
||||
Enter new secret name (alphanumeric with underscores):
|
||||
</Text>
|
||||
</Box>
|
||||
<TextInput
|
||||
value={secretName}
|
||||
onChange={onSecretNameChange}
|
||||
onSubmit={onSubmit}
|
||||
focus={true}
|
||||
placeholder="e.g., CLAUDE_API_KEY"
|
||||
columns={terminalSize.columns}
|
||||
cursorOffset={cursorOffset}
|
||||
onChangeCursorOffset={setCursorOffset}
|
||||
showCursor={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<Box marginLeft={3}>
|
||||
<Text dimColor>↑/↓ to select · Enter to continue</Text>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Text } from '../../ink.js';
|
||||
import React from 'react'
|
||||
import { Text } from '../../ink.js'
|
||||
|
||||
export function CheckGitHubStep() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <Text>Checking GitHub CLI installation…</Text>;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
return <Text>Checking GitHub CLI installation…</Text>
|
||||
}
|
||||
|
||||
@@ -1,210 +1,125 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import TextInput from '../../components/TextInput.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import TextInput from '../../components/TextInput.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||
|
||||
interface ChooseRepoStepProps {
|
||||
currentRepo: string | null;
|
||||
useCurrentRepo: boolean;
|
||||
repoUrl: string;
|
||||
onRepoUrlChange: (value: string) => void;
|
||||
onToggleUseCurrentRepo: (useCurrentRepo: boolean) => void;
|
||||
onSubmit: () => void;
|
||||
currentRepo: string | null
|
||||
useCurrentRepo: boolean
|
||||
repoUrl: string
|
||||
onRepoUrlChange: (value: string) => void
|
||||
onToggleUseCurrentRepo: (useCurrentRepo: boolean) => void
|
||||
onSubmit: () => void
|
||||
}
|
||||
export function ChooseRepoStep(t0) {
|
||||
const $ = _c(49);
|
||||
const {
|
||||
currentRepo,
|
||||
useCurrentRepo,
|
||||
repoUrl,
|
||||
onRepoUrlChange,
|
||||
onSubmit,
|
||||
onToggleUseCurrentRepo
|
||||
} = t0;
|
||||
const [cursorOffset, setCursorOffset] = useState(0);
|
||||
const [showEmptyError, setShowEmptyError] = useState(false);
|
||||
const terminalSize = useTerminalSize();
|
||||
const textInputColumns = terminalSize.columns;
|
||||
let t1;
|
||||
if ($[0] !== currentRepo || $[1] !== onSubmit || $[2] !== repoUrl || $[3] !== useCurrentRepo) {
|
||||
t1 = () => {
|
||||
const repoName = useCurrentRepo ? currentRepo : repoUrl;
|
||||
if (!repoName?.trim()) {
|
||||
setShowEmptyError(true);
|
||||
return;
|
||||
}
|
||||
onSubmit();
|
||||
};
|
||||
$[0] = currentRepo;
|
||||
$[1] = onSubmit;
|
||||
$[2] = repoUrl;
|
||||
$[3] = useCurrentRepo;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
const handleSubmit = t1;
|
||||
const isTextInputVisible = !useCurrentRepo || !currentRepo;
|
||||
let t2;
|
||||
if ($[5] !== onToggleUseCurrentRepo) {
|
||||
t2 = () => {
|
||||
onToggleUseCurrentRepo(true);
|
||||
setShowEmptyError(false);
|
||||
};
|
||||
$[5] = onToggleUseCurrentRepo;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
const handlePrevious = t2;
|
||||
let t3;
|
||||
if ($[7] !== onToggleUseCurrentRepo) {
|
||||
t3 = () => {
|
||||
onToggleUseCurrentRepo(false);
|
||||
setShowEmptyError(false);
|
||||
};
|
||||
$[7] = onToggleUseCurrentRepo;
|
||||
$[8] = t3;
|
||||
} else {
|
||||
t3 = $[8];
|
||||
}
|
||||
const handleNext = t3;
|
||||
let t4;
|
||||
if ($[9] !== handleNext || $[10] !== handlePrevious || $[11] !== handleSubmit) {
|
||||
t4 = {
|
||||
"confirm:previous": handlePrevious,
|
||||
"confirm:next": handleNext,
|
||||
"confirm:yes": handleSubmit
|
||||
};
|
||||
$[9] = handleNext;
|
||||
$[10] = handlePrevious;
|
||||
$[11] = handleSubmit;
|
||||
$[12] = t4;
|
||||
} else {
|
||||
t4 = $[12];
|
||||
}
|
||||
const t5 = !isTextInputVisible;
|
||||
let t6;
|
||||
if ($[13] !== t5) {
|
||||
t6 = {
|
||||
context: "Confirmation",
|
||||
isActive: t5
|
||||
};
|
||||
$[13] = t5;
|
||||
$[14] = t6;
|
||||
} else {
|
||||
t6 = $[14];
|
||||
}
|
||||
useKeybindings(t4, t6);
|
||||
let t7;
|
||||
if ($[15] !== handleNext || $[16] !== handlePrevious) {
|
||||
t7 = {
|
||||
"confirm:previous": handlePrevious,
|
||||
"confirm:next": handleNext
|
||||
};
|
||||
$[15] = handleNext;
|
||||
$[16] = handlePrevious;
|
||||
$[17] = t7;
|
||||
} else {
|
||||
t7 = $[17];
|
||||
}
|
||||
let t8;
|
||||
if ($[18] !== isTextInputVisible) {
|
||||
t8 = {
|
||||
context: "Confirmation",
|
||||
isActive: isTextInputVisible
|
||||
};
|
||||
$[18] = isTextInputVisible;
|
||||
$[19] = t8;
|
||||
} else {
|
||||
t8 = $[19];
|
||||
}
|
||||
useKeybindings(t7, t8);
|
||||
let t9;
|
||||
if ($[20] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = <Box flexDirection="column" marginBottom={1}><Text bold={true}>Install GitHub App</Text><Text dimColor={true}>Select GitHub repository</Text></Box>;
|
||||
$[20] = t9;
|
||||
} else {
|
||||
t9 = $[20];
|
||||
}
|
||||
let t10;
|
||||
if ($[21] !== currentRepo || $[22] !== useCurrentRepo) {
|
||||
t10 = currentRepo && <Box marginBottom={1}><Text bold={useCurrentRepo} color={useCurrentRepo ? "permission" : undefined}>{useCurrentRepo ? "> " : " "}Use current repository: {currentRepo}</Text></Box>;
|
||||
$[21] = currentRepo;
|
||||
$[22] = useCurrentRepo;
|
||||
$[23] = t10;
|
||||
} else {
|
||||
t10 = $[23];
|
||||
}
|
||||
const t11 = !useCurrentRepo || !currentRepo;
|
||||
const t12 = !useCurrentRepo || !currentRepo ? "permission" : undefined;
|
||||
const t13 = !useCurrentRepo || !currentRepo ? "> " : " ";
|
||||
const t14 = currentRepo ? "Enter a different repository" : "Enter repository";
|
||||
let t15;
|
||||
if ($[24] !== t11 || $[25] !== t12 || $[26] !== t13 || $[27] !== t14) {
|
||||
t15 = <Box marginBottom={1}><Text bold={t11} color={t12}>{t13}{t14}</Text></Box>;
|
||||
$[24] = t11;
|
||||
$[25] = t12;
|
||||
$[26] = t13;
|
||||
$[27] = t14;
|
||||
$[28] = t15;
|
||||
} else {
|
||||
t15 = $[28];
|
||||
}
|
||||
let t16;
|
||||
if ($[29] !== currentRepo || $[30] !== cursorOffset || $[31] !== handleSubmit || $[32] !== onRepoUrlChange || $[33] !== repoUrl || $[34] !== textInputColumns || $[35] !== useCurrentRepo) {
|
||||
t16 = (!useCurrentRepo || !currentRepo) && <Box marginLeft={2} marginBottom={1}><TextInput value={repoUrl} onChange={value => {
|
||||
onRepoUrlChange(value);
|
||||
setShowEmptyError(false);
|
||||
}} onSubmit={handleSubmit} focus={true} placeholder={"Enter a repo as owner/repo or https://github.com/owner/repo\u2026"} columns={textInputColumns} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} showCursor={true} /></Box>;
|
||||
$[29] = currentRepo;
|
||||
$[30] = cursorOffset;
|
||||
$[31] = handleSubmit;
|
||||
$[32] = onRepoUrlChange;
|
||||
$[33] = repoUrl;
|
||||
$[34] = textInputColumns;
|
||||
$[35] = useCurrentRepo;
|
||||
$[36] = t16;
|
||||
} else {
|
||||
t16 = $[36];
|
||||
}
|
||||
let t17;
|
||||
if ($[37] !== t10 || $[38] !== t15 || $[39] !== t16) {
|
||||
t17 = <Box flexDirection="column" borderStyle="round" paddingX={1}>{t9}{t10}{t15}{t16}</Box>;
|
||||
$[37] = t10;
|
||||
$[38] = t15;
|
||||
$[39] = t16;
|
||||
$[40] = t17;
|
||||
} else {
|
||||
t17 = $[40];
|
||||
}
|
||||
let t18;
|
||||
if ($[41] !== showEmptyError) {
|
||||
t18 = showEmptyError && <Box marginLeft={3} marginBottom={1}><Text color="error">Please enter a repository name to continue</Text></Box>;
|
||||
$[41] = showEmptyError;
|
||||
$[42] = t18;
|
||||
} else {
|
||||
t18 = $[42];
|
||||
}
|
||||
const t19 = currentRepo ? "\u2191/\u2193 to select \xB7 " : "";
|
||||
let t20;
|
||||
if ($[43] !== t19) {
|
||||
t20 = <Box marginLeft={3}><Text dimColor={true}>{t19}Enter to continue</Text></Box>;
|
||||
$[43] = t19;
|
||||
$[44] = t20;
|
||||
} else {
|
||||
t20 = $[44];
|
||||
}
|
||||
let t21;
|
||||
if ($[45] !== t17 || $[46] !== t18 || $[47] !== t20) {
|
||||
t21 = <>{t17}{t18}{t20}</>;
|
||||
$[45] = t17;
|
||||
$[46] = t18;
|
||||
$[47] = t20;
|
||||
$[48] = t21;
|
||||
} else {
|
||||
t21 = $[48];
|
||||
}
|
||||
return t21;
|
||||
|
||||
export function ChooseRepoStep({
|
||||
currentRepo,
|
||||
useCurrentRepo,
|
||||
repoUrl,
|
||||
onRepoUrlChange,
|
||||
onSubmit,
|
||||
onToggleUseCurrentRepo,
|
||||
}: ChooseRepoStepProps) {
|
||||
const [cursorOffset, setCursorOffset] = useState(0)
|
||||
const [showEmptyError, setShowEmptyError] = useState(false)
|
||||
const terminalSize = useTerminalSize()
|
||||
const textInputColumns = terminalSize.columns
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
const repoName = useCurrentRepo ? currentRepo : repoUrl
|
||||
if (!repoName?.trim()) {
|
||||
setShowEmptyError(true)
|
||||
return
|
||||
}
|
||||
onSubmit()
|
||||
}, [useCurrentRepo, currentRepo, repoUrl, onSubmit])
|
||||
|
||||
// When the text input is visible, omit confirm:yes so bare 'y' passes
|
||||
// through to the input instead of submitting. TextInput's onSubmit handles
|
||||
// Enter. Keep the Confirmation context (not Settings) to avoid j/k bindings.
|
||||
const isTextInputVisible = !useCurrentRepo || !currentRepo
|
||||
const handlePrevious = useCallback(() => {
|
||||
onToggleUseCurrentRepo(true)
|
||||
setShowEmptyError(false)
|
||||
}, [onToggleUseCurrentRepo])
|
||||
const handleNext = useCallback(() => {
|
||||
onToggleUseCurrentRepo(false)
|
||||
setShowEmptyError(false)
|
||||
}, [onToggleUseCurrentRepo])
|
||||
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:previous': handlePrevious,
|
||||
'confirm:next': handleNext,
|
||||
'confirm:yes': handleSubmit,
|
||||
},
|
||||
{ context: 'Confirmation', isActive: !isTextInputVisible },
|
||||
)
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:previous': handlePrevious,
|
||||
'confirm:next': handleNext,
|
||||
},
|
||||
{ context: 'Confirmation', isActive: isTextInputVisible },
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box flexDirection="column" borderStyle="round" paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>Install GitHub App</Text>
|
||||
<Text dimColor>Select GitHub repository</Text>
|
||||
</Box>
|
||||
{currentRepo && (
|
||||
<Box marginBottom={1}>
|
||||
<Text
|
||||
bold={useCurrentRepo}
|
||||
color={useCurrentRepo ? 'permission' : undefined}
|
||||
>
|
||||
{useCurrentRepo ? '> ' : ' '}
|
||||
Use current repository: {currentRepo}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box marginBottom={1}>
|
||||
<Text
|
||||
bold={!useCurrentRepo || !currentRepo}
|
||||
color={!useCurrentRepo || !currentRepo ? 'permission' : undefined}
|
||||
>
|
||||
{!useCurrentRepo || !currentRepo ? '> ' : ' '}
|
||||
{currentRepo ? 'Enter a different repository' : 'Enter repository'}
|
||||
</Text>
|
||||
</Box>
|
||||
{(!useCurrentRepo || !currentRepo) && (
|
||||
<Box marginLeft={2} marginBottom={1}>
|
||||
<TextInput
|
||||
value={repoUrl}
|
||||
onChange={value => {
|
||||
onRepoUrlChange(value)
|
||||
setShowEmptyError(false)
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
focus={true}
|
||||
placeholder="Enter a repo as owner/repo or https://github.com/owner/repo…"
|
||||
columns={textInputColumns}
|
||||
cursorOffset={cursorOffset}
|
||||
onChangeCursorOffset={setCursorOffset}
|
||||
showCursor={true}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{showEmptyError && (
|
||||
<Box marginLeft={3} marginBottom={1}>
|
||||
<Text color="error">Please enter a repository name to continue</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box marginLeft={3}>
|
||||
<Text dimColor>
|
||||
{currentRepo ? '↑/↓ to select · ' : ''}Enter to continue
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,64 +1,78 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { Workflow } from './types.js';
|
||||
import React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import type { Workflow } from './types.js'
|
||||
|
||||
interface CreatingStepProps {
|
||||
currentWorkflowInstallStep: number;
|
||||
secretExists: boolean;
|
||||
useExistingSecret: boolean;
|
||||
secretName: string;
|
||||
skipWorkflow?: boolean;
|
||||
selectedWorkflows: Workflow[];
|
||||
currentWorkflowInstallStep: number
|
||||
secretExists: boolean
|
||||
useExistingSecret: boolean
|
||||
secretName: string
|
||||
skipWorkflow?: boolean
|
||||
selectedWorkflows: Workflow[]
|
||||
}
|
||||
export function CreatingStep(t0) {
|
||||
const $ = _c(10);
|
||||
const {
|
||||
currentWorkflowInstallStep,
|
||||
secretExists,
|
||||
useExistingSecret,
|
||||
secretName,
|
||||
skipWorkflow: t1,
|
||||
selectedWorkflows
|
||||
} = t0;
|
||||
const skipWorkflow = t1 === undefined ? false : t1;
|
||||
let t2;
|
||||
if ($[0] !== secretExists || $[1] !== secretName || $[2] !== selectedWorkflows || $[3] !== skipWorkflow || $[4] !== useExistingSecret) {
|
||||
t2 = skipWorkflow ? ["Getting repository information", secretExists && useExistingSecret ? "Using existing API key secret" : `Setting up ${secretName} secret`] : ["Getting repository information", "Creating branch", selectedWorkflows.length > 1 ? "Creating workflow files" : "Creating workflow file", secretExists && useExistingSecret ? "Using existing API key secret" : `Setting up ${secretName} secret`, "Opening pull request page"];
|
||||
$[0] = secretExists;
|
||||
$[1] = secretName;
|
||||
$[2] = selectedWorkflows;
|
||||
$[3] = skipWorkflow;
|
||||
$[4] = useExistingSecret;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
const progressSteps = t2;
|
||||
let t3;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Box flexDirection="column" marginBottom={1}><Text bold={true}>Install GitHub App</Text><Text dimColor={true}>Create GitHub Actions workflow</Text></Box>;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
let t4;
|
||||
if ($[7] !== currentWorkflowInstallStep || $[8] !== progressSteps) {
|
||||
t4 = <><Box flexDirection="column" borderStyle="round" paddingX={1}>{t3}{progressSteps.map((stepText, index) => {
|
||||
let status = "pending";
|
||||
|
||||
export function CreatingStep({
|
||||
currentWorkflowInstallStep,
|
||||
secretExists,
|
||||
useExistingSecret,
|
||||
secretName,
|
||||
skipWorkflow = false,
|
||||
selectedWorkflows,
|
||||
}: CreatingStepProps) {
|
||||
const progressSteps = skipWorkflow
|
||||
? [
|
||||
'Getting repository information',
|
||||
secretExists && useExistingSecret
|
||||
? 'Using existing API key secret'
|
||||
: `Setting up ${secretName} secret`,
|
||||
]
|
||||
: [
|
||||
'Getting repository information',
|
||||
'Creating branch',
|
||||
selectedWorkflows.length > 1
|
||||
? 'Creating workflow files'
|
||||
: 'Creating workflow file',
|
||||
secretExists && useExistingSecret
|
||||
? 'Using existing API key secret'
|
||||
: `Setting up ${secretName} secret`,
|
||||
'Opening pull request page',
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box flexDirection="column" borderStyle="round" paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>Install GitHub App</Text>
|
||||
<Text dimColor>Create GitHub Actions workflow</Text>
|
||||
</Box>
|
||||
{progressSteps.map((stepText, index) => {
|
||||
let status: 'completed' | 'in-progress' | 'pending' = 'pending'
|
||||
|
||||
if (index < currentWorkflowInstallStep) {
|
||||
status = "completed";
|
||||
} else {
|
||||
if (index === currentWorkflowInstallStep) {
|
||||
status = "in-progress";
|
||||
}
|
||||
status = 'completed'
|
||||
} else if (index === currentWorkflowInstallStep) {
|
||||
status = 'in-progress'
|
||||
}
|
||||
return <Box key={index}><Text color={status === "completed" ? "success" : status === "in-progress" ? "warning" : undefined}>{status === "completed" ? "\u2713 " : ""}{stepText}{status === "in-progress" ? "\u2026" : ""}</Text></Box>;
|
||||
})}</Box></>;
|
||||
$[7] = currentWorkflowInstallStep;
|
||||
$[8] = progressSteps;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
return t4;
|
||||
|
||||
return (
|
||||
<Box key={index}>
|
||||
<Text
|
||||
color={
|
||||
status === 'completed'
|
||||
? 'success'
|
||||
: status === 'in-progress'
|
||||
? 'warning'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{status === 'completed' ? '✓ ' : ''}
|
||||
{stepText}
|
||||
{status === 'in-progress' ? '…' : ''}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,84 +1,51 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import React from 'react'
|
||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
|
||||
interface ErrorStepProps {
|
||||
error: string | undefined;
|
||||
errorReason?: string;
|
||||
errorInstructions?: string[];
|
||||
error: string | undefined
|
||||
errorReason?: string
|
||||
errorInstructions?: string[]
|
||||
}
|
||||
export function ErrorStep(t0) {
|
||||
const $ = _c(15);
|
||||
const {
|
||||
error,
|
||||
errorReason,
|
||||
errorInstructions
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Box flexDirection="column" marginBottom={1}><Text bold={true}>Install GitHub App</Text></Box>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
let t2;
|
||||
if ($[1] !== error) {
|
||||
t2 = <Text color="error">Error: {error}</Text>;
|
||||
$[1] = error;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== errorReason) {
|
||||
t3 = errorReason && <Box marginTop={1}><Text dimColor={true}>Reason: {errorReason}</Text></Box>;
|
||||
$[3] = errorReason;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== errorInstructions) {
|
||||
t4 = errorInstructions && errorInstructions.length > 0 && <Box flexDirection="column" marginTop={1}><Text dimColor={true}>How to fix:</Text>{errorInstructions.map(_temp)}</Box>;
|
||||
$[5] = errorInstructions;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
let t5;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Box marginTop={1}><Text dimColor={true}>For manual setup instructions, see:{" "}<Text color="claude">{GITHUB_ACTION_SETUP_DOCS_URL}</Text></Text></Box>;
|
||||
$[7] = t5;
|
||||
} else {
|
||||
t5 = $[7];
|
||||
}
|
||||
let t6;
|
||||
if ($[8] !== t2 || $[9] !== t3 || $[10] !== t4) {
|
||||
t6 = <Box flexDirection="column" borderStyle="round" paddingX={1}>{t1}{t2}{t3}{t4}{t5}</Box>;
|
||||
$[8] = t2;
|
||||
$[9] = t3;
|
||||
$[10] = t4;
|
||||
$[11] = t6;
|
||||
} else {
|
||||
t6 = $[11];
|
||||
}
|
||||
let t7;
|
||||
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Box marginLeft={3}><Text dimColor={true}>Press any key to exit</Text></Box>;
|
||||
$[12] = t7;
|
||||
} else {
|
||||
t7 = $[12];
|
||||
}
|
||||
let t8;
|
||||
if ($[13] !== t6) {
|
||||
t8 = <>{t6}{t7}</>;
|
||||
$[13] = t6;
|
||||
$[14] = t8;
|
||||
} else {
|
||||
t8 = $[14];
|
||||
}
|
||||
return t8;
|
||||
}
|
||||
function _temp(instruction, index) {
|
||||
return <Box key={index} marginLeft={2}><Text dimColor={true}>• </Text><Text>{instruction}</Text></Box>;
|
||||
|
||||
export function ErrorStep({
|
||||
error,
|
||||
errorReason,
|
||||
errorInstructions,
|
||||
}: ErrorStepProps) {
|
||||
return (
|
||||
<>
|
||||
<Box flexDirection="column" borderStyle="round" paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>Install GitHub App</Text>
|
||||
</Box>
|
||||
<Text color="error">Error: {error}</Text>
|
||||
{errorReason && (
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Reason: {errorReason}</Text>
|
||||
</Box>
|
||||
)}
|
||||
{errorInstructions && errorInstructions.length > 0 && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text dimColor>How to fix:</Text>
|
||||
{errorInstructions.map((instruction, index) => (
|
||||
<Box key={index} marginLeft={2}>
|
||||
<Text dimColor>• </Text>
|
||||
<Text>{instruction}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
For manual setup instructions, see:{' '}
|
||||
<Text color="claude">{GITHUB_ACTION_SETUP_DOCS_URL}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box marginLeft={3}>
|
||||
<Text dimColor>Press any key to exit</Text>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,102 +1,70 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Select } from 'src/components/CustomSelect/index.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import React from 'react'
|
||||
import { Select } from 'src/components/CustomSelect/index.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
|
||||
interface ExistingWorkflowStepProps {
|
||||
repoName: string;
|
||||
onSelectAction: (action: 'update' | 'skip' | 'exit') => void;
|
||||
repoName: string
|
||||
onSelectAction: (action: 'update' | 'skip' | 'exit') => void
|
||||
}
|
||||
export function ExistingWorkflowStep(t0) {
|
||||
const $ = _c(16);
|
||||
const {
|
||||
repoName,
|
||||
onSelectAction
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = [{
|
||||
label: "Update workflow file with latest version",
|
||||
value: "update"
|
||||
}, {
|
||||
label: "Skip workflow update (configure secrets only)",
|
||||
value: "skip"
|
||||
}, {
|
||||
label: "Exit without making changes",
|
||||
value: "exit"
|
||||
}];
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
|
||||
export function ExistingWorkflowStep({
|
||||
repoName,
|
||||
onSelectAction,
|
||||
}: ExistingWorkflowStepProps) {
|
||||
const options = [
|
||||
{
|
||||
label: 'Update workflow file with latest version',
|
||||
value: 'update',
|
||||
},
|
||||
{
|
||||
label: 'Skip workflow update (configure secrets only)',
|
||||
value: 'skip',
|
||||
},
|
||||
{
|
||||
label: 'Exit without making changes',
|
||||
value: 'exit',
|
||||
},
|
||||
]
|
||||
|
||||
const handleSelect = (value: string) => {
|
||||
onSelectAction(value as 'update' | 'skip' | 'exit')
|
||||
}
|
||||
const options = t1;
|
||||
let t2;
|
||||
if ($[1] !== onSelectAction) {
|
||||
t2 = value => {
|
||||
onSelectAction(value as 'update' | 'skip' | 'exit');
|
||||
};
|
||||
$[1] = onSelectAction;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
|
||||
const handleCancel = () => {
|
||||
onSelectAction('exit')
|
||||
}
|
||||
const handleSelect = t2;
|
||||
let t3;
|
||||
if ($[3] !== onSelectAction) {
|
||||
t3 = () => {
|
||||
onSelectAction("exit");
|
||||
};
|
||||
$[3] = onSelectAction;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
const handleCancel = t3;
|
||||
let t4;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Text bold={true}>Existing Workflow Found</Text>;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
let t5;
|
||||
if ($[6] !== repoName) {
|
||||
t5 = <Box flexDirection="column" marginBottom={1}>{t4}<Text dimColor={true}>Repository: {repoName}</Text></Box>;
|
||||
$[6] = repoName;
|
||||
$[7] = t5;
|
||||
} else {
|
||||
t5 = $[7];
|
||||
}
|
||||
let t6;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = <Box flexDirection="column" marginBottom={1}><Text>A Claude workflow file already exists at{" "}<Text color="claude">.github/workflows/claude.yml</Text></Text><Text dimColor={true}>What would you like to do?</Text></Box>;
|
||||
$[8] = t6;
|
||||
} else {
|
||||
t6 = $[8];
|
||||
}
|
||||
let t7;
|
||||
if ($[9] !== handleCancel || $[10] !== handleSelect) {
|
||||
t7 = <Box flexDirection="column"><Select options={options} onChange={handleSelect} onCancel={handleCancel} /></Box>;
|
||||
$[9] = handleCancel;
|
||||
$[10] = handleSelect;
|
||||
$[11] = t7;
|
||||
} else {
|
||||
t7 = $[11];
|
||||
}
|
||||
let t8;
|
||||
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = <Box marginTop={1}><Text dimColor={true}>View the latest workflow template at:{" "}<Text color="claude">https://github.com/anthropics/claude-code-action/blob/main/examples/claude.yml</Text></Text></Box>;
|
||||
$[12] = t8;
|
||||
} else {
|
||||
t8 = $[12];
|
||||
}
|
||||
let t9;
|
||||
if ($[13] !== t5 || $[14] !== t7) {
|
||||
t9 = <Box flexDirection="column" borderStyle="round" borderDimColor={true} paddingX={1}>{t5}{t6}{t7}{t8}</Box>;
|
||||
$[13] = t5;
|
||||
$[14] = t7;
|
||||
$[15] = t9;
|
||||
} else {
|
||||
t9 = $[15];
|
||||
}
|
||||
return t9;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" borderStyle="round" borderDimColor paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>Existing Workflow Found</Text>
|
||||
<Text dimColor>Repository: {repoName}</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text>
|
||||
A Claude workflow file already exists at{' '}
|
||||
<Text color="claude">.github/workflows/claude.yml</Text>
|
||||
</Text>
|
||||
<Text dimColor>What would you like to do?</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<Select
|
||||
options={options}
|
||||
onChange={handleSelect}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
View the latest workflow template at:{' '}
|
||||
<Text color="claude">
|
||||
https://github.com/anthropics/claude-code-action/blob/main/examples/claude.yml
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,93 +1,53 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import React from 'react';
|
||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||||
import figures from 'figures'
|
||||
import React from 'react'
|
||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||
|
||||
interface InstallAppStepProps {
|
||||
repoUrl: string;
|
||||
onSubmit: () => void;
|
||||
repoUrl: string
|
||||
onSubmit: () => void
|
||||
}
|
||||
export function InstallAppStep(t0) {
|
||||
const $ = _c(12);
|
||||
const {
|
||||
repoUrl,
|
||||
onSubmit
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
useKeybinding("confirm:yes", onSubmit, t1);
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Box flexDirection="column" marginBottom={1}><Text bold={true}>Install the Claude GitHub App</Text></Box>;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
let t3;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Box marginBottom={1}><Text>Opening browser to install the Claude GitHub App…</Text></Box>;
|
||||
$[2] = t3;
|
||||
} else {
|
||||
t3 = $[2];
|
||||
}
|
||||
let t4;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Box marginBottom={1}><Text>If your browser doesn't open automatically, visit:</Text></Box>;
|
||||
$[3] = t4;
|
||||
} else {
|
||||
t4 = $[3];
|
||||
}
|
||||
let t5;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Box marginBottom={1}><Text underline={true}>https://github.com/apps/claude</Text></Box>;
|
||||
$[4] = t5;
|
||||
} else {
|
||||
t5 = $[4];
|
||||
}
|
||||
let t6;
|
||||
if ($[5] !== repoUrl) {
|
||||
t6 = <Box marginBottom={1}><Text>Please install the app for repository: <Text bold={true}>{repoUrl}</Text></Text></Box>;
|
||||
$[5] = repoUrl;
|
||||
$[6] = t6;
|
||||
} else {
|
||||
t6 = $[6];
|
||||
}
|
||||
let t7;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Box marginBottom={1}><Text dimColor={true}>Important: Make sure to grant access to this specific repository</Text></Box>;
|
||||
$[7] = t7;
|
||||
} else {
|
||||
t7 = $[7];
|
||||
}
|
||||
let t8;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = <Box><Text bold={true} color="permission">Press Enter once you've installed the app{figures.ellipsis}</Text></Box>;
|
||||
$[8] = t8;
|
||||
} else {
|
||||
t8 = $[8];
|
||||
}
|
||||
let t9;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = <Box marginTop={1}><Text dimColor={true}>Having trouble? See manual setup instructions at:{" "}<Text color="claude">{GITHUB_ACTION_SETUP_DOCS_URL}</Text></Text></Box>;
|
||||
$[9] = t9;
|
||||
} else {
|
||||
t9 = $[9];
|
||||
}
|
||||
let t10;
|
||||
if ($[10] !== t6) {
|
||||
t10 = <Box flexDirection="column" borderStyle="round" borderDimColor={true} paddingX={1}>{t2}{t3}{t4}{t5}{t6}{t7}{t8}{t9}</Box>;
|
||||
$[10] = t6;
|
||||
$[11] = t10;
|
||||
} else {
|
||||
t10 = $[11];
|
||||
}
|
||||
return t10;
|
||||
|
||||
export function InstallAppStep({ repoUrl, onSubmit }: InstallAppStepProps) {
|
||||
// Enter to submit
|
||||
useKeybinding('confirm:yes', onSubmit, { context: 'Confirmation' })
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" borderStyle="round" borderDimColor paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>Install the Claude GitHub App</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>
|
||||
<Text>Opening browser to install the Claude GitHub App…</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>
|
||||
<Text>If your browser doesn't open automatically, visit:</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>
|
||||
<Text underline>https://github.com/apps/claude</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>
|
||||
<Text>
|
||||
Please install the app for repository: <Text bold>{repoUrl}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>
|
||||
<Text dimColor>
|
||||
Important: Make sure to grant access to this specific repository
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text bold color="permission">
|
||||
Press Enter once you've installed the app{figures.ellipsis}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
Having trouble? See manual setup instructions at:{' '}
|
||||
<Text color="claude">{GITHUB_ACTION_SETUP_DOCS_URL}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,275 +1,343 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
|
||||
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js';
|
||||
import { Spinner } from '../../components/Spinner.js';
|
||||
import TextInput from '../../components/TextInput.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||||
import { setClipboard } from '../../ink/termio/osc.js';
|
||||
import { Box, Link, Text } from '../../ink.js';
|
||||
import { OAuthService } from '../../services/oauth/index.js';
|
||||
import { saveOAuthTokensIfNeeded } from '../../utils/auth.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from 'src/services/analytics/index.js'
|
||||
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js'
|
||||
import { Spinner } from '../../components/Spinner.js'
|
||||
import TextInput from '../../components/TextInput.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||
import { setClipboard } from '../../ink/termio/osc.js'
|
||||
import { Box, Link, Text } from '../../ink.js'
|
||||
import { OAuthService } from '../../services/oauth/index.js'
|
||||
import { saveOAuthTokensIfNeeded } from '../../utils/auth.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
|
||||
interface OAuthFlowStepProps {
|
||||
onSuccess: (token: string) => void;
|
||||
onCancel: () => void;
|
||||
onSuccess: (token: string) => void
|
||||
onCancel: () => void
|
||||
}
|
||||
type OAuthStatus = {
|
||||
state: 'starting';
|
||||
} | {
|
||||
state: 'waiting_for_login';
|
||||
url: string;
|
||||
} | {
|
||||
state: 'processing';
|
||||
} | {
|
||||
state: 'success';
|
||||
token: string;
|
||||
} | {
|
||||
state: 'error';
|
||||
message: string;
|
||||
toRetry?: OAuthStatus;
|
||||
} | {
|
||||
state: 'about_to_retry';
|
||||
nextState: OAuthStatus;
|
||||
};
|
||||
const PASTE_HERE_MSG = 'Paste code here if prompted > ';
|
||||
|
||||
type OAuthStatus =
|
||||
| { state: 'starting' }
|
||||
| { state: 'waiting_for_login'; url: string }
|
||||
| { state: 'processing' }
|
||||
| { state: 'success'; token: string }
|
||||
| { state: 'error'; message: string; toRetry?: OAuthStatus }
|
||||
| { state: 'about_to_retry'; nextState: OAuthStatus }
|
||||
|
||||
const PASTE_HERE_MSG = 'Paste code here if prompted > '
|
||||
|
||||
export function OAuthFlowStep({
|
||||
onSuccess,
|
||||
onCancel
|
||||
onCancel,
|
||||
}: OAuthFlowStepProps): React.ReactNode {
|
||||
const [oauthStatus, setOAuthStatus] = useState<OAuthStatus>({
|
||||
state: 'starting'
|
||||
});
|
||||
const [oauthService] = useState(() => new OAuthService());
|
||||
const [pastedCode, setPastedCode] = useState('');
|
||||
const [cursorOffset, setCursorOffset] = useState(0);
|
||||
const [showPastePrompt, setShowPastePrompt] = useState(false);
|
||||
const [urlCopied, setUrlCopied] = useState(false);
|
||||
const timersRef = useRef<Set<NodeJS.Timeout>>(new Set());
|
||||
state: 'starting',
|
||||
})
|
||||
const [oauthService] = useState(() => new OAuthService())
|
||||
const [pastedCode, setPastedCode] = useState('')
|
||||
const [cursorOffset, setCursorOffset] = useState(0)
|
||||
const [showPastePrompt, setShowPastePrompt] = useState(false)
|
||||
const [urlCopied, setUrlCopied] = useState(false)
|
||||
const timersRef = useRef<Set<NodeJS.Timeout>>(new Set())
|
||||
// Separate ref so startOAuth's timer clear doesn't cancel the urlCopied reset
|
||||
const urlCopiedTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||
const terminalSize = useTerminalSize();
|
||||
const textInputColumns = Math.max(50, terminalSize.columns - PASTE_HERE_MSG.length - 4);
|
||||
const urlCopiedTimerRef = useRef<NodeJS.Timeout | undefined>(undefined)
|
||||
|
||||
const terminalSize = useTerminalSize()
|
||||
const textInputColumns = Math.max(
|
||||
50,
|
||||
terminalSize.columns - PASTE_HERE_MSG.length - 4,
|
||||
)
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent): void {
|
||||
if (oauthStatus.state !== 'error') return;
|
||||
e.preventDefault();
|
||||
if (oauthStatus.state !== 'error') return
|
||||
e.preventDefault()
|
||||
if (e.key === 'return' && oauthStatus.toRetry) {
|
||||
setPastedCode('');
|
||||
setCursorOffset(0);
|
||||
setPastedCode('')
|
||||
setCursorOffset(0)
|
||||
setOAuthStatus({
|
||||
state: 'about_to_retry',
|
||||
nextState: oauthStatus.toRetry
|
||||
});
|
||||
nextState: oauthStatus.toRetry,
|
||||
})
|
||||
} else {
|
||||
onCancel();
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmitCode(value: string, url: string) {
|
||||
try {
|
||||
// Expecting format "authorizationCode#state" from the authorization callback URL
|
||||
const [authorizationCode, state] = value.split('#');
|
||||
const [authorizationCode, state] = value.split('#')
|
||||
|
||||
if (!authorizationCode || !state) {
|
||||
setOAuthStatus({
|
||||
state: 'error',
|
||||
message: 'Invalid code. Please make sure the full code was copied',
|
||||
toRetry: {
|
||||
state: 'waiting_for_login',
|
||||
url
|
||||
}
|
||||
});
|
||||
return;
|
||||
toRetry: { state: 'waiting_for_login', url },
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Track which path the user is taking (manual code entry)
|
||||
logEvent('tengu_oauth_manual_entry', {});
|
||||
logEvent('tengu_oauth_manual_entry', {})
|
||||
oauthService.handleManualAuthCodeInput({
|
||||
authorizationCode,
|
||||
state
|
||||
});
|
||||
state,
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
logError(err);
|
||||
logError(err)
|
||||
setOAuthStatus({
|
||||
state: 'error',
|
||||
message: (err as Error).message,
|
||||
toRetry: {
|
||||
state: 'waiting_for_login',
|
||||
url
|
||||
}
|
||||
});
|
||||
toRetry: { state: 'waiting_for_login', url },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const startOAuth = useCallback(async () => {
|
||||
// Clear any existing timers when starting new OAuth flow
|
||||
timersRef.current.forEach(timer => clearTimeout(timer));
|
||||
timersRef.current.clear();
|
||||
timersRef.current.forEach(timer => clearTimeout(timer))
|
||||
timersRef.current.clear()
|
||||
|
||||
try {
|
||||
const result = await oauthService.startOAuthFlow(async url_0 => {
|
||||
setOAuthStatus({
|
||||
state: 'waiting_for_login',
|
||||
url: url_0
|
||||
});
|
||||
const timer_0 = setTimeout(setShowPastePrompt, 3000, true);
|
||||
timersRef.current.add(timer_0);
|
||||
}, {
|
||||
loginWithClaudeAi: true,
|
||||
// Always use Claude AI for subscription tokens
|
||||
inferenceOnly: true,
|
||||
expiresIn: 365 * 24 * 60 * 60 // 1 year
|
||||
});
|
||||
const result = await oauthService.startOAuthFlow(
|
||||
async url => {
|
||||
setOAuthStatus({ state: 'waiting_for_login', url })
|
||||
const timer = setTimeout(setShowPastePrompt, 3000, true)
|
||||
timersRef.current.add(timer)
|
||||
},
|
||||
{
|
||||
loginWithClaudeAi: true, // Always use Claude AI for subscription tokens
|
||||
inferenceOnly: true,
|
||||
expiresIn: 365 * 24 * 60 * 60, // 1 year
|
||||
},
|
||||
)
|
||||
|
||||
// Show processing state
|
||||
setOAuthStatus({
|
||||
state: 'processing'
|
||||
});
|
||||
setOAuthStatus({ state: 'processing' })
|
||||
|
||||
// OAuthFlowStep creates inference-only tokens for GitHub Actions, not a
|
||||
// replacement login. Use saveOAuthTokensIfNeeded directly to avoid
|
||||
// performLogout which would destroy the user's existing auth session.
|
||||
saveOAuthTokensIfNeeded(result);
|
||||
saveOAuthTokensIfNeeded(result)
|
||||
|
||||
// For OAuth flow, the access token can be used as an API key
|
||||
const timer1 = setTimeout((setOAuthStatus_0, accessToken, onSuccess_0, timersRef_0) => {
|
||||
setOAuthStatus_0({
|
||||
state: 'success',
|
||||
token: accessToken
|
||||
});
|
||||
// Auto-continue after brief delay to show success
|
||||
const timer2 = setTimeout(onSuccess_0, 1000, accessToken);
|
||||
timersRef_0.current.add(timer2 as ReturnType<typeof setTimeout>);
|
||||
}, 100, setOAuthStatus, result.accessToken, onSuccess, timersRef);
|
||||
timersRef.current.add(timer1);
|
||||
} catch (err_0) {
|
||||
const errorMessage = (err_0 as Error).message;
|
||||
const timer1 = setTimeout(
|
||||
(setOAuthStatus, accessToken, onSuccess, timersRef) => {
|
||||
setOAuthStatus({ state: 'success', token: accessToken })
|
||||
// Auto-continue after brief delay to show success
|
||||
const timer2 = setTimeout(onSuccess, 1000, accessToken)
|
||||
timersRef.current.add(timer2)
|
||||
},
|
||||
100,
|
||||
setOAuthStatus,
|
||||
result.accessToken,
|
||||
onSuccess,
|
||||
timersRef,
|
||||
)
|
||||
timersRef.current.add(timer1)
|
||||
} catch (err) {
|
||||
const errorMessage = (err as Error).message
|
||||
setOAuthStatus({
|
||||
state: 'error',
|
||||
message: errorMessage,
|
||||
toRetry: {
|
||||
state: 'starting'
|
||||
} // Allow retry by starting fresh OAuth flow
|
||||
});
|
||||
logError(err_0);
|
||||
toRetry: { state: 'starting' }, // Allow retry by starting fresh OAuth flow
|
||||
})
|
||||
logError(err)
|
||||
logEvent('tengu_oauth_error', {
|
||||
error: errorMessage as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
error:
|
||||
errorMessage as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
}
|
||||
}, [oauthService, onSuccess]);
|
||||
}, [oauthService, onSuccess])
|
||||
|
||||
useEffect(() => {
|
||||
if (oauthStatus.state === 'starting') {
|
||||
void startOAuth();
|
||||
void startOAuth()
|
||||
}
|
||||
}, [oauthStatus.state, startOAuth]);
|
||||
}, [oauthStatus.state, startOAuth])
|
||||
|
||||
// Retry logic
|
||||
useEffect(() => {
|
||||
if (oauthStatus.state === 'about_to_retry') {
|
||||
const timer_1 = setTimeout((nextState, setShowPastePrompt_0, setOAuthStatus_1) => {
|
||||
// Only show paste prompt when retrying to waiting_for_login
|
||||
setShowPastePrompt_0(nextState.state === 'waiting_for_login');
|
||||
setOAuthStatus_1(nextState);
|
||||
}, 500, oauthStatus.nextState, setShowPastePrompt, setOAuthStatus);
|
||||
timersRef.current.add(timer_1);
|
||||
const timer = setTimeout(
|
||||
(nextState, setShowPastePrompt, setOAuthStatus) => {
|
||||
// Only show paste prompt when retrying to waiting_for_login
|
||||
setShowPastePrompt(nextState.state === 'waiting_for_login')
|
||||
setOAuthStatus(nextState)
|
||||
},
|
||||
500,
|
||||
oauthStatus.nextState,
|
||||
setShowPastePrompt,
|
||||
setOAuthStatus,
|
||||
)
|
||||
timersRef.current.add(timer)
|
||||
}
|
||||
}, [oauthStatus]);
|
||||
}, [oauthStatus])
|
||||
|
||||
useEffect(() => {
|
||||
if (pastedCode === 'c' && oauthStatus.state === 'waiting_for_login' && showPastePrompt && !urlCopied) {
|
||||
if (
|
||||
pastedCode === 'c' &&
|
||||
oauthStatus.state === 'waiting_for_login' &&
|
||||
showPastePrompt &&
|
||||
!urlCopied
|
||||
) {
|
||||
void setClipboard(oauthStatus.url).then(raw => {
|
||||
if (raw) process.stdout.write(raw);
|
||||
setUrlCopied(true);
|
||||
clearTimeout(urlCopiedTimerRef.current);
|
||||
urlCopiedTimerRef.current = setTimeout(setUrlCopied, 2000, false);
|
||||
});
|
||||
setPastedCode('');
|
||||
if (raw) process.stdout.write(raw)
|
||||
setUrlCopied(true)
|
||||
clearTimeout(urlCopiedTimerRef.current)
|
||||
urlCopiedTimerRef.current = setTimeout(setUrlCopied, 2000, false)
|
||||
})
|
||||
setPastedCode('')
|
||||
}
|
||||
}, [pastedCode, oauthStatus, showPastePrompt, urlCopied]);
|
||||
}, [pastedCode, oauthStatus, showPastePrompt, urlCopied])
|
||||
|
||||
// Cleanup OAuth service and timers when component unmounts
|
||||
useEffect(() => {
|
||||
const timers = timersRef.current;
|
||||
const timers = timersRef.current
|
||||
return () => {
|
||||
oauthService.cleanup();
|
||||
oauthService.cleanup()
|
||||
// Clear all timers
|
||||
timers.forEach(timer_2 => clearTimeout(timer_2));
|
||||
timers.clear();
|
||||
clearTimeout(urlCopiedTimerRef.current);
|
||||
};
|
||||
}, [oauthService]);
|
||||
timers.forEach(timer => clearTimeout(timer))
|
||||
timers.clear()
|
||||
clearTimeout(urlCopiedTimerRef.current)
|
||||
}
|
||||
}, [oauthService])
|
||||
|
||||
// Helper function to render the appropriate status message
|
||||
function renderStatusMessage(): React.ReactNode {
|
||||
switch (oauthStatus.state) {
|
||||
case 'starting':
|
||||
return <Box>
|
||||
return (
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text>Starting authentication…</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
|
||||
case 'waiting_for_login':
|
||||
return <Box flexDirection="column" gap={1}>
|
||||
{!showPastePrompt && <Box>
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
{!showPastePrompt && (
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text>
|
||||
Opening browser to sign in with your Claude account…
|
||||
</Text>
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{showPastePrompt && <Box>
|
||||
{showPastePrompt && (
|
||||
<Box>
|
||||
<Text>{PASTE_HERE_MSG}</Text>
|
||||
<TextInput value={pastedCode} onChange={setPastedCode} onSubmit={(value_0: string) => handleSubmitCode(value_0, oauthStatus.url)} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} columns={textInputColumns} />
|
||||
</Box>}
|
||||
</Box>;
|
||||
<TextInput
|
||||
value={pastedCode}
|
||||
onChange={setPastedCode}
|
||||
onSubmit={(value: string) =>
|
||||
handleSubmitCode(value, oauthStatus.url)
|
||||
}
|
||||
cursorOffset={cursorOffset}
|
||||
onChangeCursorOffset={setCursorOffset}
|
||||
columns={textInputColumns}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
|
||||
case 'processing':
|
||||
return <Box>
|
||||
return (
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text>Processing authentication…</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
|
||||
case 'success':
|
||||
return <Box flexDirection="column" gap={1}>
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text color="success">
|
||||
✓ Authentication token created successfully!
|
||||
</Text>
|
||||
<Text dimColor>Using token for GitHub Actions setup…</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
|
||||
case 'error':
|
||||
return <Box flexDirection="column" gap={1}>
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text color="error">OAuth error: {oauthStatus.message}</Text>
|
||||
{oauthStatus.toRetry ? <Text dimColor>
|
||||
{oauthStatus.toRetry ? (
|
||||
<Text dimColor>
|
||||
Press Enter to try again, or any other key to cancel
|
||||
</Text> : <Text dimColor>Press any key to return to API key selection</Text>}
|
||||
</Box>;
|
||||
</Text>
|
||||
) : (
|
||||
<Text dimColor>Press any key to return to API key selection</Text>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
|
||||
case 'about_to_retry':
|
||||
return <Box flexDirection="column" gap={1}>
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text color="permission">Retrying…</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
|
||||
default:
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
}
|
||||
return <Box flexDirection="column" gap={1} tabIndex={0} autoFocus onKeyDown={handleKeyDown}>
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
gap={1}
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{/* Show header inline only for initial starting state */}
|
||||
{oauthStatus.state === 'starting' && <Box flexDirection="column" gap={1} paddingBottom={1}>
|
||||
{oauthStatus.state === 'starting' && (
|
||||
<Box flexDirection="column" gap={1} paddingBottom={1}>
|
||||
<Text bold>Create Authentication Token</Text>
|
||||
<Text dimColor>Creating a long-lived token for GitHub Actions</Text>
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
{/* Show header for non-starting states (to avoid duplicate with inline header)*/}
|
||||
{oauthStatus.state !== 'success' && oauthStatus.state !== 'starting' && oauthStatus.state !== 'processing' && <Box key="header" flexDirection="column" gap={1} paddingBottom={1}>
|
||||
{oauthStatus.state !== 'success' &&
|
||||
oauthStatus.state !== 'starting' &&
|
||||
oauthStatus.state !== 'processing' && (
|
||||
<Box key="header" flexDirection="column" gap={1} paddingBottom={1}>
|
||||
<Text bold>Create Authentication Token</Text>
|
||||
<Text dimColor>Creating a long-lived token for GitHub Actions</Text>
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
{/* Show URL when paste prompt is visible */}
|
||||
{oauthStatus.state === 'waiting_for_login' && showPastePrompt && <Box flexDirection="column" key="urlToCopy" gap={1} paddingBottom={1}>
|
||||
{oauthStatus.state === 'waiting_for_login' && showPastePrompt && (
|
||||
<Box flexDirection="column" key="urlToCopy" gap={1} paddingBottom={1}>
|
||||
<Box paddingX={1}>
|
||||
<Text dimColor>
|
||||
Browser didn't open? Use the url below to sign in{' '}
|
||||
</Text>
|
||||
{urlCopied ? <Text color="success">(Copied!)</Text> : <Text dimColor>
|
||||
{urlCopied ? (
|
||||
<Text color="success">(Copied!)</Text>
|
||||
) : (
|
||||
<Text dimColor>
|
||||
<KeyboardShortcutHint shortcut="c" action="copy" parens />
|
||||
</Text>}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Link url={oauthStatus.url}>
|
||||
<Text dimColor>{oauthStatus.url}</Text>
|
||||
</Link>
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
<Box paddingLeft={1} flexDirection="column" gap={1}>
|
||||
{renderStatusMessage()}
|
||||
</Box>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,95 +1,65 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
|
||||
type SuccessStepProps = {
|
||||
secretExists: boolean;
|
||||
useExistingSecret: boolean;
|
||||
secretName: string;
|
||||
skipWorkflow?: boolean;
|
||||
};
|
||||
export function SuccessStep(t0) {
|
||||
const $ = _c(21);
|
||||
const {
|
||||
secretExists,
|
||||
useExistingSecret,
|
||||
secretName,
|
||||
skipWorkflow: t1
|
||||
} = t0;
|
||||
const skipWorkflow = t1 === undefined ? false : t1;
|
||||
let t2;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Box flexDirection="column" marginBottom={1}><Text bold={true}>Install GitHub App</Text><Text dimColor={true}>Success</Text></Box>;
|
||||
$[0] = t2;
|
||||
} else {
|
||||
t2 = $[0];
|
||||
}
|
||||
let t3;
|
||||
if ($[1] !== skipWorkflow) {
|
||||
t3 = !skipWorkflow && <Text color="success">✓ GitHub Actions workflow created!</Text>;
|
||||
$[1] = skipWorkflow;
|
||||
$[2] = t3;
|
||||
} else {
|
||||
t3 = $[2];
|
||||
}
|
||||
let t4;
|
||||
if ($[3] !== secretExists || $[4] !== useExistingSecret) {
|
||||
t4 = secretExists && useExistingSecret && <Box marginTop={1}><Text color="success">✓ Using existing ANTHROPIC_API_KEY secret</Text></Box>;
|
||||
$[3] = secretExists;
|
||||
$[4] = useExistingSecret;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
let t5;
|
||||
if ($[6] !== secretExists || $[7] !== secretName || $[8] !== useExistingSecret) {
|
||||
t5 = (!secretExists || !useExistingSecret) && <Box marginTop={1}><Text color="success">✓ API key saved as {secretName} secret</Text></Box>;
|
||||
$[6] = secretExists;
|
||||
$[7] = secretName;
|
||||
$[8] = useExistingSecret;
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
let t6;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = <Box marginTop={1}><Text>Next steps:</Text></Box>;
|
||||
$[10] = t6;
|
||||
} else {
|
||||
t6 = $[10];
|
||||
}
|
||||
let t7;
|
||||
if ($[11] !== skipWorkflow) {
|
||||
t7 = skipWorkflow ? <><Text>1. Install the Claude GitHub App if you haven't already</Text><Text>2. Your workflow file was kept unchanged</Text><Text>3. API key is configured and ready to use</Text></> : <><Text>1. A pre-filled PR page has been created</Text><Text>2. Install the Claude GitHub App if you haven't already</Text><Text>3. Merge the PR to enable Claude PR assistance</Text></>;
|
||||
$[11] = skipWorkflow;
|
||||
$[12] = t7;
|
||||
} else {
|
||||
t7 = $[12];
|
||||
}
|
||||
let t8;
|
||||
if ($[13] !== t3 || $[14] !== t4 || $[15] !== t5 || $[16] !== t7) {
|
||||
t8 = <Box flexDirection="column" borderStyle="round" paddingX={1}>{t2}{t3}{t4}{t5}{t6}{t7}</Box>;
|
||||
$[13] = t3;
|
||||
$[14] = t4;
|
||||
$[15] = t5;
|
||||
$[16] = t7;
|
||||
$[17] = t8;
|
||||
} else {
|
||||
t8 = $[17];
|
||||
}
|
||||
let t9;
|
||||
if ($[18] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = <Box marginLeft={3}><Text dimColor={true}>Press any key to exit</Text></Box>;
|
||||
$[18] = t9;
|
||||
} else {
|
||||
t9 = $[18];
|
||||
}
|
||||
let t10;
|
||||
if ($[19] !== t8) {
|
||||
t10 = <>{t8}{t9}</>;
|
||||
$[19] = t8;
|
||||
$[20] = t10;
|
||||
} else {
|
||||
t10 = $[20];
|
||||
}
|
||||
return t10;
|
||||
secretExists: boolean
|
||||
useExistingSecret: boolean
|
||||
secretName: string
|
||||
skipWorkflow?: boolean
|
||||
}
|
||||
|
||||
export function SuccessStep({
|
||||
secretExists,
|
||||
useExistingSecret,
|
||||
secretName,
|
||||
skipWorkflow = false,
|
||||
}: SuccessStepProps): React.ReactNode {
|
||||
return (
|
||||
<>
|
||||
<Box flexDirection="column" borderStyle="round" paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>Install GitHub App</Text>
|
||||
<Text dimColor>Success</Text>
|
||||
</Box>
|
||||
{!skipWorkflow && (
|
||||
<Text color="success">✓ GitHub Actions workflow created!</Text>
|
||||
)}
|
||||
{secretExists && useExistingSecret && (
|
||||
<Box marginTop={1}>
|
||||
<Text color="success">
|
||||
✓ Using existing ANTHROPIC_API_KEY secret
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{(!secretExists || !useExistingSecret) && (
|
||||
<Box marginTop={1}>
|
||||
<Text color="success">✓ API key saved as {secretName} secret</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box marginTop={1}>
|
||||
<Text>Next steps:</Text>
|
||||
</Box>
|
||||
{skipWorkflow ? (
|
||||
<>
|
||||
<Text>
|
||||
1. Install the Claude GitHub App if you haven't already
|
||||
</Text>
|
||||
<Text>2. Your workflow file was kept unchanged</Text>
|
||||
<Text>3. API key is configured and ready to use</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text>1. A pre-filled PR page has been created</Text>
|
||||
<Text>
|
||||
2. Install the Claude GitHub App if you haven't already
|
||||
</Text>
|
||||
<Text>3. Merge the PR to enable Claude PR assistance</Text>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<Box marginLeft={3}>
|
||||
<Text dimColor>Press any key to exit</Text>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,72 +1,59 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import React from 'react';
|
||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||||
import type { Warning } from './types.js';
|
||||
import figures from 'figures'
|
||||
import React from 'react'
|
||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||
import type { Warning } from './types.js'
|
||||
|
||||
interface WarningsStepProps {
|
||||
warnings: Warning[];
|
||||
onContinue: () => void;
|
||||
warnings: Warning[]
|
||||
onContinue: () => void
|
||||
}
|
||||
export function WarningsStep(t0) {
|
||||
const $ = _c(8);
|
||||
const {
|
||||
warnings,
|
||||
onContinue
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
useKeybinding("confirm:yes", onContinue, t1);
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Box flexDirection="column" marginBottom={1}><Text bold={true}>{figures.warning} Setup Warnings</Text><Text dimColor={true}>We found some potential issues, but you can continue anyway</Text></Box>;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
let t3;
|
||||
if ($[2] !== warnings) {
|
||||
t3 = warnings.map(_temp2);
|
||||
$[2] = warnings;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
let t4;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Box marginTop={1}><Text bold={true} color="permission">Press Enter to continue anyway, or Ctrl+C to exit and fix issues</Text></Box>;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
let t5;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Box marginTop={1}><Text dimColor={true}>You can also try the manual setup steps if needed:{" "}<Text color="claude">{GITHUB_ACTION_SETUP_DOCS_URL}</Text></Text></Box>;
|
||||
$[5] = t5;
|
||||
} else {
|
||||
t5 = $[5];
|
||||
}
|
||||
let t6;
|
||||
if ($[6] !== t3) {
|
||||
t6 = <><Box flexDirection="column" borderStyle="round" paddingX={1}>{t2}{t3}{t4}{t5}</Box></>;
|
||||
$[6] = t3;
|
||||
$[7] = t6;
|
||||
} else {
|
||||
t6 = $[7];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
function _temp2(warning, index) {
|
||||
return <Box key={index} flexDirection="column" marginBottom={1}><Text color="warning" bold={true}>{warning.title}</Text><Text>{warning.message}</Text>{warning.instructions.length > 0 && <Box flexDirection="column" marginLeft={2} marginTop={1}>{warning.instructions.map(_temp)}</Box>}</Box>;
|
||||
}
|
||||
function _temp(instruction, i) {
|
||||
return <Text key={i} dimColor={true}>• {instruction}</Text>;
|
||||
|
||||
export function WarningsStep({ warnings, onContinue }: WarningsStepProps) {
|
||||
// Enter to continue
|
||||
useKeybinding('confirm:yes', onContinue, { context: 'Confirmation' })
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box flexDirection="column" borderStyle="round" paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>{figures.warning} Setup Warnings</Text>
|
||||
<Text dimColor>
|
||||
We found some potential issues, but you can continue anyway
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{warnings.map((warning, index) => (
|
||||
<Box key={index} flexDirection="column" marginBottom={1}>
|
||||
<Text color="warning" bold>
|
||||
{warning.title}
|
||||
</Text>
|
||||
<Text>{warning.message}</Text>
|
||||
{warning.instructions.length > 0 && (
|
||||
<Box flexDirection="column" marginLeft={2} marginTop={1}>
|
||||
{warning.instructions.map((instruction, i) => (
|
||||
<Text key={i} dimColor>
|
||||
• {instruction}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text bold color="permission">
|
||||
Press Enter to continue anyway, or Ctrl+C to exit and fix issues
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
You can also try the manual setup steps if needed:{' '}
|
||||
<Text color="claude">{GITHUB_ACTION_SETUP_DOCS_URL}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,239 +1,252 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { CommandResultDisplay } from 'src/commands.js';
|
||||
import { logEvent } from 'src/services/analytics/index.js';
|
||||
import { StatusIcon } from '../components/design-system/StatusIcon.js';
|
||||
import { Box, render, Text } from '../ink.js';
|
||||
import { logForDebugging } from '../utils/debug.js';
|
||||
import { env } from '../utils/env.js';
|
||||
import { errorMessage } from '../utils/errors.js';
|
||||
import { checkInstall, cleanupNpmInstallations, cleanupShellAliases, installLatest } from '../utils/nativeInstaller/index.js';
|
||||
import { getInitialSettings, updateSettingsForSource } from '../utils/settings/settings.js';
|
||||
import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import type { CommandResultDisplay } from 'src/commands.js'
|
||||
import { logEvent } from 'src/services/analytics/index.js'
|
||||
import { StatusIcon } from '../components/design-system/StatusIcon.js'
|
||||
import { Box, render, Text } from '../ink.js'
|
||||
import { logForDebugging } from '../utils/debug.js'
|
||||
import { env } from '../utils/env.js'
|
||||
import { errorMessage } from '../utils/errors.js'
|
||||
import {
|
||||
checkInstall,
|
||||
cleanupNpmInstallations,
|
||||
cleanupShellAliases,
|
||||
installLatest,
|
||||
} from '../utils/nativeInstaller/index.js'
|
||||
import {
|
||||
getInitialSettings,
|
||||
updateSettingsForSource,
|
||||
} from '../utils/settings/settings.js'
|
||||
|
||||
interface InstallProps {
|
||||
onDone: (result: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
force?: boolean;
|
||||
target?: string; // 'latest', 'stable', or version like '1.0.34'
|
||||
onDone: (result: string, options?: { display?: CommandResultDisplay }) => void
|
||||
force?: boolean
|
||||
target?: string // 'latest', 'stable', or version like '1.0.34'
|
||||
}
|
||||
type InstallState = {
|
||||
type: 'checking';
|
||||
} | {
|
||||
type: 'cleaning-npm';
|
||||
} | {
|
||||
type: 'installing';
|
||||
version: string;
|
||||
} | {
|
||||
type: 'setting-up';
|
||||
} | {
|
||||
type: 'set-up';
|
||||
messages: string[];
|
||||
} | {
|
||||
type: 'success';
|
||||
version: string;
|
||||
setupMessages?: string[];
|
||||
} | {
|
||||
type: 'error';
|
||||
message: string;
|
||||
warnings?: string[];
|
||||
};
|
||||
|
||||
type InstallState =
|
||||
| { type: 'checking' }
|
||||
| { type: 'cleaning-npm' }
|
||||
| { type: 'installing'; version: string }
|
||||
| { type: 'setting-up' }
|
||||
| { type: 'set-up'; messages: string[] }
|
||||
| { type: 'success'; version: string; setupMessages?: string[] }
|
||||
| { type: 'error'; message: string; warnings?: string[] }
|
||||
|
||||
function getInstallationPath(): string {
|
||||
const isWindows = env.platform === 'win32';
|
||||
const homeDir = homedir();
|
||||
const isWindows = env.platform === 'win32'
|
||||
const homeDir = homedir()
|
||||
|
||||
if (isWindows) {
|
||||
// Convert to Windows-style path
|
||||
const windowsPath = join(homeDir, '.local', 'bin', 'claude.exe');
|
||||
const windowsPath = join(homeDir, '.local', 'bin', 'claude.exe')
|
||||
// Replace forward slashes with backslashes for Windows display
|
||||
return windowsPath.replace(/\//g, '\\');
|
||||
return windowsPath.replace(/\//g, '\\')
|
||||
}
|
||||
return '~/.local/bin/claude';
|
||||
|
||||
return '~/.local/bin/claude'
|
||||
}
|
||||
function SetupNotes(t0) {
|
||||
const $ = _c(5);
|
||||
const {
|
||||
messages
|
||||
} = t0;
|
||||
if (messages.length === 0) {
|
||||
return null;
|
||||
}
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Box><Text color="warning"><StatusIcon status="warning" withSpace={true} />Setup notes:</Text></Box>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
let t2;
|
||||
if ($[1] !== messages) {
|
||||
t2 = messages.map(_temp);
|
||||
$[1] = messages;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== t2) {
|
||||
t3 = <Box flexDirection="column" gap={0} marginBottom={1}>{t1}{t2}</Box>;
|
||||
$[3] = t2;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
|
||||
function SetupNotes({ messages }: { messages: string[] }): React.ReactNode {
|
||||
if (messages.length === 0) return null
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={0} marginBottom={1}>
|
||||
<Box>
|
||||
<Text color="warning">
|
||||
<StatusIcon status="warning" withSpace />
|
||||
Setup notes:
|
||||
</Text>
|
||||
</Box>
|
||||
{messages.map((message, index) => (
|
||||
<Box key={index} marginLeft={2}>
|
||||
<Text dimColor>• {message}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
function _temp(message, index) {
|
||||
return <Box key={index} marginLeft={2}><Text dimColor={true}>• {message}</Text></Box>;
|
||||
}
|
||||
function Install({
|
||||
onDone,
|
||||
force,
|
||||
target
|
||||
}: InstallProps): React.ReactNode {
|
||||
const [state, setState] = useState<InstallState>({
|
||||
type: 'checking'
|
||||
});
|
||||
|
||||
function Install({ onDone, force, target }: InstallProps): React.ReactNode {
|
||||
const [state, setState] = useState<InstallState>({ type: 'checking' })
|
||||
|
||||
useEffect(() => {
|
||||
async function run() {
|
||||
try {
|
||||
logForDebugging(`Install: Starting installation process (force=${force}, target=${target})`);
|
||||
logForDebugging(
|
||||
`Install: Starting installation process (force=${force}, target=${target})`,
|
||||
)
|
||||
|
||||
// Install native build first
|
||||
const channelOrVersion = target || getInitialSettings()?.autoUpdatesChannel || 'latest';
|
||||
setState({
|
||||
type: 'installing',
|
||||
version: channelOrVersion
|
||||
});
|
||||
const channelOrVersion =
|
||||
target || getInitialSettings()?.autoUpdatesChannel || 'latest'
|
||||
setState({ type: 'installing', version: channelOrVersion })
|
||||
|
||||
// Pass force flag to trigger reinstall even if up to date
|
||||
logForDebugging(`Install: Calling installLatest(channelOrVersion=${channelOrVersion}, forceReinstall=${force})`);
|
||||
const result = await installLatest(channelOrVersion, force);
|
||||
logForDebugging(`Install: installLatest returned version=${result.latestVersion}, wasUpdated=${result.wasUpdated}, lockFailed=${result.lockFailed}`);
|
||||
logForDebugging(
|
||||
`Install: Calling installLatest(channelOrVersion=${channelOrVersion}, forceReinstall=${force})`,
|
||||
)
|
||||
const result = await installLatest(channelOrVersion, force)
|
||||
logForDebugging(
|
||||
`Install: installLatest returned version=${result.latestVersion}, wasUpdated=${result.wasUpdated}, lockFailed=${result.lockFailed}`,
|
||||
)
|
||||
|
||||
// Check specifically for lock failure
|
||||
if (result.lockFailed) {
|
||||
throw new Error('Could not install - another process is currently installing Claude. Please try again in a moment.');
|
||||
throw new Error(
|
||||
'Could not install - another process is currently installing Claude. Please try again in a moment.',
|
||||
)
|
||||
}
|
||||
|
||||
// If we couldn't get the version, there might be an issue
|
||||
if (!result.latestVersion) {
|
||||
logForDebugging('Install: Failed to retrieve version information during install', {
|
||||
level: 'error'
|
||||
});
|
||||
logForDebugging(
|
||||
'Install: Failed to retrieve version information during install',
|
||||
{ level: 'error' },
|
||||
)
|
||||
}
|
||||
|
||||
if (!result.wasUpdated) {
|
||||
logForDebugging('Install: Already up to date');
|
||||
logForDebugging('Install: Already up to date')
|
||||
}
|
||||
|
||||
// Set up launcher and shell integration
|
||||
setState({
|
||||
type: 'setting-up'
|
||||
});
|
||||
const setupMessages = await checkInstall(true);
|
||||
logForDebugging(`Install: Setup launcher completed with ${setupMessages.length} messages`);
|
||||
setState({ type: 'setting-up' })
|
||||
const setupMessages = await checkInstall(true)
|
||||
|
||||
logForDebugging(
|
||||
`Install: Setup launcher completed with ${setupMessages.length} messages`,
|
||||
)
|
||||
if (setupMessages.length > 0) {
|
||||
setupMessages.forEach(msg => logForDebugging(`Install: Setup message: ${msg.message}`));
|
||||
setupMessages.forEach(msg =>
|
||||
logForDebugging(`Install: Setup message: ${msg.message}`),
|
||||
)
|
||||
}
|
||||
|
||||
// Now that native installation succeeded, clean up old npm installations
|
||||
logForDebugging('Install: Cleaning up npm installations after successful install');
|
||||
const {
|
||||
removed,
|
||||
errors,
|
||||
warnings
|
||||
} = await cleanupNpmInstallations();
|
||||
logForDebugging(
|
||||
'Install: Cleaning up npm installations after successful install',
|
||||
)
|
||||
const { removed, errors, warnings } = await cleanupNpmInstallations()
|
||||
|
||||
if (removed > 0) {
|
||||
logForDebugging(`Cleaned up ${removed} npm installation(s)`);
|
||||
logForDebugging(`Cleaned up ${removed} npm installation(s)`)
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
logForDebugging(`Cleanup errors: ${errors.join(', ')}`);
|
||||
logForDebugging(`Cleanup errors: ${errors.join(', ')}`)
|
||||
// Continue despite cleanup errors - native install already succeeded
|
||||
}
|
||||
|
||||
// Clean up old shell aliases
|
||||
const aliasMessages = await cleanupShellAliases();
|
||||
const aliasMessages = await cleanupShellAliases()
|
||||
if (aliasMessages.length > 0) {
|
||||
logForDebugging(`Shell alias cleanup: ${aliasMessages.map(m => m.message).join('; ')}`);
|
||||
logForDebugging(
|
||||
`Shell alias cleanup: ${aliasMessages.map(m => m.message).join('; ')}`,
|
||||
)
|
||||
}
|
||||
|
||||
// Log success event
|
||||
logEvent('tengu_claude_install_command', {
|
||||
has_version: result.latestVersion ? 1 : 0,
|
||||
forced: force ? 1 : 0
|
||||
});
|
||||
forced: force ? 1 : 0,
|
||||
})
|
||||
|
||||
// If user explicitly specified a channel, save it to settings
|
||||
if (target === 'latest' || target === 'stable') {
|
||||
updateSettingsForSource('userSettings', {
|
||||
autoUpdatesChannel: target
|
||||
});
|
||||
logForDebugging(`Install: Saved autoUpdatesChannel=${target} to user settings`);
|
||||
autoUpdatesChannel: target,
|
||||
})
|
||||
logForDebugging(
|
||||
`Install: Saved autoUpdatesChannel=${target} to user settings`,
|
||||
)
|
||||
}
|
||||
|
||||
// Combine all warning/info messages (convert SetupMessage to string)
|
||||
const allWarnings = [...warnings, ...aliasMessages.map(m_0 => m_0.message)];
|
||||
const allWarnings = [...warnings, ...aliasMessages.map(m => m.message)]
|
||||
|
||||
// Check if there were any setup errors or notes
|
||||
if (setupMessages.length > 0) {
|
||||
setState({
|
||||
type: 'set-up',
|
||||
messages: setupMessages.map(m_1 => m_1.message)
|
||||
});
|
||||
messages: setupMessages.map(m => m.message),
|
||||
})
|
||||
// Still mark as success but show both setup messages and cleanup warnings
|
||||
setTimeout(setState, 2000, {
|
||||
type: 'success' as const,
|
||||
version: result.latestVersion || 'current',
|
||||
setupMessages: [...setupMessages.map(m_2 => m_2.message), ...allWarnings]
|
||||
});
|
||||
setupMessages: [
|
||||
...setupMessages.map(m => m.message),
|
||||
...allWarnings,
|
||||
],
|
||||
})
|
||||
} else {
|
||||
// No setup messages, go straight to success (but still show cleanup warnings if any)
|
||||
logForDebugging('Install: Shell PATH already configured');
|
||||
logForDebugging('Install: Shell PATH already configured')
|
||||
setState({
|
||||
type: 'success',
|
||||
version: result.latestVersion || 'current',
|
||||
setupMessages: allWarnings.length > 0 ? allWarnings : undefined
|
||||
});
|
||||
setupMessages: allWarnings.length > 0 ? allWarnings : undefined,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
logForDebugging(`Install command failed: ${error}`, {
|
||||
level: 'error'
|
||||
});
|
||||
level: 'error',
|
||||
})
|
||||
setState({
|
||||
type: 'error',
|
||||
message: errorMessage(error)
|
||||
});
|
||||
message: errorMessage(error),
|
||||
})
|
||||
}
|
||||
}
|
||||
void run();
|
||||
}, [force, target]);
|
||||
|
||||
void run()
|
||||
}, [force, target])
|
||||
|
||||
useEffect(() => {
|
||||
if (state.type === 'success') {
|
||||
// Give success message time to render before exiting
|
||||
setTimeout(onDone, 2000, 'Claude Code installation completed successfully', {
|
||||
display: 'system' as const
|
||||
});
|
||||
setTimeout(
|
||||
onDone,
|
||||
2000,
|
||||
'Claude Code installation completed successfully',
|
||||
{
|
||||
display: 'system' as const,
|
||||
},
|
||||
)
|
||||
} else if (state.type === 'error') {
|
||||
// Give error message time to render before exiting
|
||||
setTimeout(onDone, 3000, 'Claude Code installation failed', {
|
||||
display: 'system' as const
|
||||
});
|
||||
display: 'system' as const,
|
||||
})
|
||||
}
|
||||
}, [state, onDone]);
|
||||
return <Box flexDirection="column" marginTop={1}>
|
||||
{state.type === 'checking' && <Text color="claude">Checking installation status...</Text>}
|
||||
}, [state, onDone])
|
||||
|
||||
{state.type === 'cleaning-npm' && <Text color="warning">Cleaning up old npm installations...</Text>}
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
{state.type === 'checking' && (
|
||||
<Text color="claude">Checking installation status...</Text>
|
||||
)}
|
||||
|
||||
{state.type === 'installing' && <Text color="claude">
|
||||
{state.type === 'cleaning-npm' && (
|
||||
<Text color="warning">Cleaning up old npm installations...</Text>
|
||||
)}
|
||||
|
||||
{state.type === 'installing' && (
|
||||
<Text color="claude">
|
||||
Installing Claude Code native build {state.version}...
|
||||
</Text>}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{state.type === 'setting-up' && <Text color="claude">Setting up launcher and shell integration...</Text>}
|
||||
{state.type === 'setting-up' && (
|
||||
<Text color="claude">Setting up launcher and shell integration...</Text>
|
||||
)}
|
||||
|
||||
{state.type === 'set-up' && <SetupNotes messages={state.messages} />}
|
||||
|
||||
{state.type === 'success' && <Box flexDirection="column" gap={1}>
|
||||
{state.type === 'success' && (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box>
|
||||
<StatusIcon status="success" withSpace />
|
||||
<Text color="success" bold>
|
||||
@@ -241,10 +254,12 @@ function Install({
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginLeft={2} flexDirection="column" gap={1}>
|
||||
{state.version !== 'current' && <Box>
|
||||
{state.version !== 'current' && (
|
||||
<Box>
|
||||
<Text dimColor>Version: </Text>
|
||||
<Text color="claude">{state.version}</Text>
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
<Box>
|
||||
<Text dimColor>Location: </Text>
|
||||
<Text color="text">{getInstallationPath()}</Text>
|
||||
@@ -260,9 +275,11 @@ function Install({
|
||||
</Box>
|
||||
</Box>
|
||||
{state.setupMessages && <SetupNotes messages={state.setupMessages} />}
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{state.type === 'error' && <Box flexDirection="column" gap={1}>
|
||||
{state.type === 'error' && (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box>
|
||||
<StatusIcon status="error" withSpace />
|
||||
<Text color="error">Installation failed</Text>
|
||||
@@ -271,8 +288,10 @@ function Install({
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Try running with --force to override checks</Text>
|
||||
</Box>
|
||||
</Box>}
|
||||
</Box>;
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// This is only used from cli.tsx, not as a slash command
|
||||
@@ -281,19 +300,28 @@ export const install = {
|
||||
name: 'install',
|
||||
description: 'Install Claude Code native build',
|
||||
argumentHint: '[options]',
|
||||
async call(onDone: (result: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void, _context: unknown, args: string[]) {
|
||||
async call(
|
||||
onDone: (
|
||||
result: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void,
|
||||
_context: unknown,
|
||||
args: string[],
|
||||
) {
|
||||
// Parse arguments
|
||||
const force = args.includes('--force');
|
||||
const nonFlagArgs = args.filter(arg => !arg.startsWith('--'));
|
||||
const target = nonFlagArgs[0]; // 'latest', 'stable', or version like '1.0.34'
|
||||
const force = args.includes('--force')
|
||||
const nonFlagArgs = args.filter(arg => !arg.startsWith('--'))
|
||||
const target = nonFlagArgs[0] // 'latest', 'stable', or version like '1.0.34'
|
||||
|
||||
const {
|
||||
unmount
|
||||
} = await render(<Install onDone={(result, options) => {
|
||||
unmount();
|
||||
onDone(result, options);
|
||||
}} force={force} target={target} />);
|
||||
}
|
||||
};
|
||||
const { unmount } = await render(
|
||||
<Install
|
||||
onDone={(result, options) => {
|
||||
unmount()
|
||||
onDone(result, options)
|
||||
}}
|
||||
force={force}
|
||||
target={target}
|
||||
/>,
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,103 +1,113 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import * as React from 'react';
|
||||
import { resetCostState } from '../../bootstrap/state.js';
|
||||
import { clearTrustedDeviceToken, enrollTrustedDevice } from '../../bridge/trustedDevice.js';
|
||||
import type { LocalJSXCommandContext } from '../../commands.js';
|
||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js';
|
||||
import { ConsoleOAuthFlow } from '../../components/ConsoleOAuthFlow.js';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js';
|
||||
import { Text } from '../../ink.js';
|
||||
import { refreshGrowthBookAfterAuthChange } from '../../services/analytics/growthbook.js';
|
||||
import { refreshPolicyLimits } from '../../services/policyLimits/index.js';
|
||||
import { refreshRemoteManagedSettings } from '../../services/remoteManagedSettings/index.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { stripSignatureBlocks } from '../../utils/messages.js';
|
||||
import { checkAndDisableAutoModeIfNeeded, checkAndDisableBypassPermissionsIfNeeded, resetAutoModeGateCheck, resetBypassPermissionsCheck } from '../../utils/permissions/bypassPermissionsKillswitch.js';
|
||||
import { resetUserCache } from '../../utils/user.js';
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext): Promise<React.ReactNode> {
|
||||
return <Login onDone={async success => {
|
||||
context.onChangeAPIKey();
|
||||
// Signature-bearing blocks (thinking, connector_text) are bound to the API key —
|
||||
// strip them so the new key doesn't reject stale signatures.
|
||||
context.setMessages(stripSignatureBlocks);
|
||||
if (success) {
|
||||
// Post-login refresh logic. Keep in sync with onboarding in src/interactiveHelpers.tsx
|
||||
// Reset cost state when switching accounts
|
||||
resetCostState();
|
||||
// Refresh remotely managed settings after login (non-blocking)
|
||||
void refreshRemoteManagedSettings();
|
||||
// Refresh policy limits after login (non-blocking)
|
||||
void refreshPolicyLimits();
|
||||
// Clear user data cache BEFORE GrowthBook refresh so it picks up fresh credentials
|
||||
resetUserCache();
|
||||
// Refresh GrowthBook after login to get updated feature flags (e.g., for claude.ai MCPs)
|
||||
refreshGrowthBookAfterAuthChange();
|
||||
// Clear any stale trusted device token from a previous account before
|
||||
// re-enrolling — prevents sending the old token on bridge calls while
|
||||
// the async enrollTrustedDevice() is in-flight.
|
||||
clearTrustedDeviceToken();
|
||||
// Enroll as a trusted device for Remote Control (10-min fresh-session window)
|
||||
void enrollTrustedDevice();
|
||||
// Reset killswitch gate checks and re-run with new org
|
||||
resetBypassPermissionsCheck();
|
||||
const appState = context.getAppState();
|
||||
void checkAndDisableBypassPermissionsIfNeeded(appState.toolPermissionContext, context.setAppState);
|
||||
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
||||
resetAutoModeGateCheck();
|
||||
void checkAndDisableAutoModeIfNeeded(appState.toolPermissionContext, context.setAppState, appState.fastMode);
|
||||
import { feature } from 'bun:bundle'
|
||||
import * as React from 'react'
|
||||
import { resetCostState } from '../../bootstrap/state.js'
|
||||
import {
|
||||
clearTrustedDeviceToken,
|
||||
enrollTrustedDevice,
|
||||
} from '../../bridge/trustedDevice.js'
|
||||
import type { LocalJSXCommandContext } from '../../commands.js'
|
||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
|
||||
import { ConsoleOAuthFlow } from '../../components/ConsoleOAuthFlow.js'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'
|
||||
import { Text } from '../../ink.js'
|
||||
import { refreshGrowthBookAfterAuthChange } from '../../services/analytics/growthbook.js'
|
||||
import { refreshPolicyLimits } from '../../services/policyLimits/index.js'
|
||||
import { refreshRemoteManagedSettings } from '../../services/remoteManagedSettings/index.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import { stripSignatureBlocks } from '../../utils/messages.js'
|
||||
import {
|
||||
checkAndDisableAutoModeIfNeeded,
|
||||
checkAndDisableBypassPermissionsIfNeeded,
|
||||
resetAutoModeGateCheck,
|
||||
resetBypassPermissionsCheck,
|
||||
} from '../../utils/permissions/bypassPermissionsKillswitch.js'
|
||||
import { resetUserCache } from '../../utils/user.js'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
): Promise<React.ReactNode> {
|
||||
return (
|
||||
<Login
|
||||
onDone={async success => {
|
||||
context.onChangeAPIKey()
|
||||
// Signature-bearing blocks (thinking, connector_text) are bound to the API key —
|
||||
// strip them so the new key doesn't reject stale signatures.
|
||||
context.setMessages(stripSignatureBlocks)
|
||||
if (success) {
|
||||
// Post-login refresh logic. Keep in sync with onboarding in src/interactiveHelpers.tsx
|
||||
// Reset cost state when switching accounts
|
||||
resetCostState()
|
||||
// Refresh remotely managed settings after login (non-blocking)
|
||||
void refreshRemoteManagedSettings()
|
||||
// Refresh policy limits after login (non-blocking)
|
||||
void refreshPolicyLimits()
|
||||
// Clear user data cache BEFORE GrowthBook refresh so it picks up fresh credentials
|
||||
resetUserCache()
|
||||
// Refresh GrowthBook after login to get updated feature flags (e.g., for claude.ai MCPs)
|
||||
refreshGrowthBookAfterAuthChange()
|
||||
// Clear any stale trusted device token from a previous account before
|
||||
// re-enrolling — prevents sending the old token on bridge calls while
|
||||
// the async enrollTrustedDevice() is in-flight.
|
||||
clearTrustedDeviceToken()
|
||||
// Enroll as a trusted device for Remote Control (10-min fresh-session window)
|
||||
void enrollTrustedDevice()
|
||||
// Reset killswitch gate checks and re-run with new org
|
||||
resetBypassPermissionsCheck()
|
||||
const appState = context.getAppState()
|
||||
void checkAndDisableBypassPermissionsIfNeeded(
|
||||
appState.toolPermissionContext,
|
||||
context.setAppState,
|
||||
)
|
||||
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
||||
resetAutoModeGateCheck()
|
||||
void checkAndDisableAutoModeIfNeeded(
|
||||
appState.toolPermissionContext,
|
||||
context.setAppState,
|
||||
appState.fastMode,
|
||||
)
|
||||
}
|
||||
// Increment authVersion to trigger re-fetching of auth-dependent data in hooks (e.g., MCP servers)
|
||||
context.setAppState(prev => ({
|
||||
...prev,
|
||||
authVersion: prev.authVersion + 1,
|
||||
}))
|
||||
}
|
||||
onDone(success ? 'Login successful' : 'Login interrupted')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function Login(props: {
|
||||
onDone: (success: boolean, mainLoopModel: string) => void
|
||||
startingMessage?: string
|
||||
}): React.ReactNode {
|
||||
const mainLoopModel = useMainLoopModel()
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="Login"
|
||||
onCancel={() => props.onDone(false, mainLoopModel)}
|
||||
color="permission"
|
||||
inputGuide={exitState =>
|
||||
exitState.pending ? (
|
||||
<Text>Press {exitState.keyName} again to exit</Text>
|
||||
) : (
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="cancel"
|
||||
/>
|
||||
)
|
||||
}
|
||||
// Increment authVersion to trigger re-fetching of auth-dependent data in hooks (e.g., MCP servers)
|
||||
context.setAppState(prev => ({
|
||||
...prev,
|
||||
authVersion: prev.authVersion + 1
|
||||
}));
|
||||
}
|
||||
onDone(success ? 'Login successful' : 'Login interrupted');
|
||||
}} />;
|
||||
}
|
||||
export function Login(props) {
|
||||
const $ = _c(12);
|
||||
const mainLoopModel = useMainLoopModel();
|
||||
let t0;
|
||||
if ($[0] !== mainLoopModel || $[1] !== props) {
|
||||
t0 = () => props.onDone(false, mainLoopModel);
|
||||
$[0] = mainLoopModel;
|
||||
$[1] = props;
|
||||
$[2] = t0;
|
||||
} else {
|
||||
t0 = $[2];
|
||||
}
|
||||
let t1;
|
||||
if ($[3] !== mainLoopModel || $[4] !== props) {
|
||||
t1 = () => props.onDone(true, mainLoopModel);
|
||||
$[3] = mainLoopModel;
|
||||
$[4] = props;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
let t2;
|
||||
if ($[6] !== props.startingMessage || $[7] !== t1) {
|
||||
t2 = <ConsoleOAuthFlow onDone={t1} startingMessage={props.startingMessage} />;
|
||||
$[6] = props.startingMessage;
|
||||
$[7] = t1;
|
||||
$[8] = t2;
|
||||
} else {
|
||||
t2 = $[8];
|
||||
}
|
||||
let t3;
|
||||
if ($[9] !== t0 || $[10] !== t2) {
|
||||
t3 = <Dialog title="Login" onCancel={t0} color="permission" inputGuide={_temp}>{t2}</Dialog>;
|
||||
$[9] = t0;
|
||||
$[10] = t2;
|
||||
$[11] = t3;
|
||||
} else {
|
||||
t3 = $[11];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
function _temp(exitState) {
|
||||
return exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" />;
|
||||
>
|
||||
<ConsoleOAuthFlow
|
||||
onDone={() => props.onDone(true, mainLoopModel)}
|
||||
startingMessage={props.startingMessage}
|
||||
/>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,81 +1,89 @@
|
||||
import * as React from 'react';
|
||||
import { clearTrustedDeviceTokenCache } from '../../bridge/trustedDevice.js';
|
||||
import { Text } from '../../ink.js';
|
||||
import { refreshGrowthBookAfterAuthChange } from '../../services/analytics/growthbook.js';
|
||||
import { getGroveNoticeConfig, getGroveSettings } from '../../services/api/grove.js';
|
||||
import { clearPolicyLimitsCache } from '../../services/policyLimits/index.js';
|
||||
import * as React from 'react'
|
||||
import { clearTrustedDeviceTokenCache } from '../../bridge/trustedDevice.js'
|
||||
import { Text } from '../../ink.js'
|
||||
import { refreshGrowthBookAfterAuthChange } from '../../services/analytics/growthbook.js'
|
||||
import {
|
||||
getGroveNoticeConfig,
|
||||
getGroveSettings,
|
||||
} from '../../services/api/grove.js'
|
||||
import { clearPolicyLimitsCache } from '../../services/policyLimits/index.js'
|
||||
// flushTelemetry is loaded lazily to avoid pulling in ~1.1MB of OpenTelemetry at startup
|
||||
import { clearRemoteManagedSettingsCache } from '../../services/remoteManagedSettings/index.js';
|
||||
import { getClaudeAIOAuthTokens, removeApiKey } from '../../utils/auth.js';
|
||||
import { clearBetasCaches } from '../../utils/betas.js';
|
||||
import { saveGlobalConfig } from '../../utils/config.js';
|
||||
import { gracefulShutdownSync } from '../../utils/gracefulShutdown.js';
|
||||
import { getSecureStorage } from '../../utils/secureStorage/index.js';
|
||||
import { clearToolSchemaCache } from '../../utils/toolSchemaCache.js';
|
||||
import { resetUserCache } from '../../utils/user.js';
|
||||
import { clearRemoteManagedSettingsCache } from '../../services/remoteManagedSettings/index.js'
|
||||
import { getClaudeAIOAuthTokens, removeApiKey } from '../../utils/auth.js'
|
||||
import { clearBetasCaches } from '../../utils/betas.js'
|
||||
import { saveGlobalConfig } from '../../utils/config.js'
|
||||
import { gracefulShutdownSync } from '../../utils/gracefulShutdown.js'
|
||||
import { getSecureStorage } from '../../utils/secureStorage/index.js'
|
||||
import { clearToolSchemaCache } from '../../utils/toolSchemaCache.js'
|
||||
import { resetUserCache } from '../../utils/user.js'
|
||||
|
||||
export async function performLogout({
|
||||
clearOnboarding = false
|
||||
clearOnboarding = false,
|
||||
}): Promise<void> {
|
||||
// Flush telemetry BEFORE clearing credentials to prevent org data leakage
|
||||
const {
|
||||
flushTelemetry
|
||||
} = await import('../../utils/telemetry/instrumentation.js');
|
||||
await flushTelemetry();
|
||||
await removeApiKey();
|
||||
const { flushTelemetry } = await import(
|
||||
'../../utils/telemetry/instrumentation.js'
|
||||
)
|
||||
await flushTelemetry()
|
||||
|
||||
await removeApiKey()
|
||||
|
||||
// Wipe all secure storage data on logout
|
||||
const secureStorage = getSecureStorage();
|
||||
secureStorage.delete();
|
||||
await clearAuthRelatedCaches();
|
||||
const secureStorage = getSecureStorage()
|
||||
secureStorage.delete()
|
||||
|
||||
await clearAuthRelatedCaches()
|
||||
saveGlobalConfig(current => {
|
||||
const updated = {
|
||||
...current
|
||||
};
|
||||
const updated = { ...current }
|
||||
if (clearOnboarding) {
|
||||
updated.hasCompletedOnboarding = false;
|
||||
updated.subscriptionNoticeCount = 0;
|
||||
updated.hasAvailableSubscription = false;
|
||||
updated.hasCompletedOnboarding = false
|
||||
updated.subscriptionNoticeCount = 0
|
||||
updated.hasAvailableSubscription = false
|
||||
if (updated.customApiKeyResponses?.approved) {
|
||||
updated.customApiKeyResponses = {
|
||||
...updated.customApiKeyResponses,
|
||||
approved: []
|
||||
};
|
||||
approved: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
updated.oauthAccount = undefined;
|
||||
return updated;
|
||||
});
|
||||
updated.oauthAccount = undefined
|
||||
return updated
|
||||
})
|
||||
}
|
||||
|
||||
// clearing anything memoized that must be invalidated when user/session/auth changes
|
||||
export async function clearAuthRelatedCaches(): Promise<void> {
|
||||
// Clear the OAuth token cache
|
||||
getClaudeAIOAuthTokens.cache?.clear?.();
|
||||
clearTrustedDeviceTokenCache();
|
||||
clearBetasCaches();
|
||||
clearToolSchemaCache();
|
||||
getClaudeAIOAuthTokens.cache?.clear?.()
|
||||
clearTrustedDeviceTokenCache()
|
||||
clearBetasCaches()
|
||||
clearToolSchemaCache()
|
||||
|
||||
// Clear user data cache BEFORE GrowthBook refresh so it picks up fresh credentials
|
||||
resetUserCache();
|
||||
refreshGrowthBookAfterAuthChange();
|
||||
resetUserCache()
|
||||
refreshGrowthBookAfterAuthChange()
|
||||
|
||||
// Clear Grove config cache
|
||||
getGroveNoticeConfig.cache?.clear?.();
|
||||
getGroveSettings.cache?.clear?.();
|
||||
getGroveNoticeConfig.cache?.clear?.()
|
||||
getGroveSettings.cache?.clear?.()
|
||||
|
||||
// Clear remotely managed settings cache
|
||||
await clearRemoteManagedSettingsCache();
|
||||
await clearRemoteManagedSettingsCache()
|
||||
|
||||
// Clear policy limits cache
|
||||
await clearPolicyLimitsCache();
|
||||
await clearPolicyLimitsCache()
|
||||
}
|
||||
|
||||
export async function call(): Promise<React.ReactNode> {
|
||||
await performLogout({
|
||||
clearOnboarding: true
|
||||
});
|
||||
const message = <Text>Successfully logged out from your Anthropic account.</Text>;
|
||||
await performLogout({ clearOnboarding: true })
|
||||
|
||||
const message = (
|
||||
<Text>Successfully logged out from your Anthropic account.</Text>
|
||||
)
|
||||
|
||||
setTimeout(() => {
|
||||
gracefulShutdownSync(0, 'logout');
|
||||
}, 200);
|
||||
return message;
|
||||
gracefulShutdownSync(0, 'logout')
|
||||
}, 200)
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
@@ -1,84 +1,105 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { MCPSettings } from '../../components/mcp/index.js';
|
||||
import { MCPReconnect } from '../../components/mcp/MCPReconnect.js';
|
||||
import { useMcpToggleEnabled } from '../../services/mcp/MCPConnectionManager.js';
|
||||
import { useAppState } from '../../state/AppState.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { PluginSettings } from '../plugin/PluginSettings.js';
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { MCPSettings } from '../../components/mcp/index.js'
|
||||
import { MCPReconnect } from '../../components/mcp/MCPReconnect.js'
|
||||
import { useMcpToggleEnabled } from '../../services/mcp/MCPConnectionManager.js'
|
||||
import { useAppState } from '../../state/AppState.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import { PluginSettings } from '../plugin/PluginSettings.js'
|
||||
|
||||
// TODO: This is a hack to get the context value from toggleMcpServer (useContext only works in a component)
|
||||
// Ideally, all MCP state and functions would be in global state.
|
||||
function MCPToggle(t0) {
|
||||
const $ = _c(7);
|
||||
const {
|
||||
action,
|
||||
target,
|
||||
onComplete
|
||||
} = t0;
|
||||
const mcpClients = useAppState(_temp);
|
||||
const toggleMcpServer = useMcpToggleEnabled();
|
||||
const didRun = useRef(false);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== action || $[1] !== mcpClients || $[2] !== onComplete || $[3] !== target || $[4] !== toggleMcpServer) {
|
||||
t1 = () => {
|
||||
if (didRun.current) {
|
||||
return;
|
||||
}
|
||||
didRun.current = true;
|
||||
const isEnabling = action === "enable";
|
||||
const clients = mcpClients.filter(_temp2);
|
||||
const toToggle = target === "all" ? clients.filter(c_0 => isEnabling ? c_0.type === "disabled" : c_0.type !== "disabled") : clients.filter(c_1 => c_1.name === target);
|
||||
if (toToggle.length === 0) {
|
||||
onComplete(target === "all" ? `All MCP servers are already ${isEnabling ? "enabled" : "disabled"}` : `MCP server "${target}" not found`);
|
||||
return;
|
||||
}
|
||||
for (const s_0 of toToggle) {
|
||||
toggleMcpServer(s_0.name);
|
||||
}
|
||||
onComplete(target === "all" ? `${isEnabling ? "Enabled" : "Disabled"} ${toToggle.length} MCP server(s)` : `MCP server "${target}" ${isEnabling ? "enabled" : "disabled"}`);
|
||||
};
|
||||
t2 = [action, target, mcpClients, toggleMcpServer, onComplete];
|
||||
$[0] = action;
|
||||
$[1] = mcpClients;
|
||||
$[2] = onComplete;
|
||||
$[3] = target;
|
||||
$[4] = toggleMcpServer;
|
||||
$[5] = t1;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
t2 = $[6];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
return null;
|
||||
function MCPToggle({
|
||||
action,
|
||||
target,
|
||||
onComplete,
|
||||
}: {
|
||||
action: 'enable' | 'disable'
|
||||
target: string
|
||||
onComplete: (result: string) => void
|
||||
}): null {
|
||||
const mcpClients = useAppState(s => s.mcp.clients)
|
||||
const toggleMcpServer = useMcpToggleEnabled()
|
||||
const didRun = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (didRun.current) return
|
||||
didRun.current = true
|
||||
|
||||
const isEnabling = action === 'enable'
|
||||
const clients = mcpClients.filter(c => c.name !== 'ide')
|
||||
const toToggle =
|
||||
target === 'all'
|
||||
? clients.filter(c =>
|
||||
isEnabling ? c.type === 'disabled' : c.type !== 'disabled',
|
||||
)
|
||||
: clients.filter(c => c.name === target)
|
||||
|
||||
if (toToggle.length === 0) {
|
||||
onComplete(
|
||||
target === 'all'
|
||||
? `All MCP servers are already ${isEnabling ? 'enabled' : 'disabled'}`
|
||||
: `MCP server "${target}" not found`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
for (const s of toToggle) {
|
||||
void toggleMcpServer(s.name)
|
||||
}
|
||||
|
||||
onComplete(
|
||||
target === 'all'
|
||||
? `${isEnabling ? 'Enabled' : 'Disabled'} ${toToggle.length} MCP server(s)`
|
||||
: `MCP server "${target}" ${isEnabling ? 'enabled' : 'disabled'}`,
|
||||
)
|
||||
}, [action, target, mcpClients, toggleMcpServer, onComplete])
|
||||
|
||||
return null
|
||||
}
|
||||
function _temp2(c) {
|
||||
return c.name !== "ide";
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.mcp.clients;
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, _context: unknown, args?: string): Promise<React.ReactNode> {
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
_context: unknown,
|
||||
args?: string,
|
||||
): Promise<React.ReactNode> {
|
||||
if (args) {
|
||||
const parts = args.trim().split(/\s+/);
|
||||
const parts = args.trim().split(/\s+/)
|
||||
|
||||
// Allow /mcp no-redirect to bypass the redirect for testing
|
||||
if (parts[0] === 'no-redirect') {
|
||||
return <MCPSettings onComplete={onDone} />;
|
||||
return <MCPSettings onComplete={onDone} />
|
||||
}
|
||||
|
||||
if (parts[0] === 'reconnect' && parts[1]) {
|
||||
return <MCPReconnect serverName={parts.slice(1).join(' ')} onComplete={onDone} />;
|
||||
return (
|
||||
<MCPReconnect
|
||||
serverName={parts.slice(1).join(' ')}
|
||||
onComplete={onDone}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (parts[0] === 'enable' || parts[0] === 'disable') {
|
||||
return <MCPToggle action={parts[0]} target={parts.length > 1 ? parts.slice(1).join(' ') : 'all'} onComplete={onDone} />;
|
||||
return (
|
||||
<MCPToggle
|
||||
action={parts[0]}
|
||||
target={parts.length > 1 ? parts.slice(1).join(' ') : 'all'}
|
||||
onComplete={onDone}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect base /mcp command to /plugins installed tab for ant users
|
||||
if ((process.env.USER_TYPE) === 'ant') {
|
||||
return <PluginSettings onComplete={onDone} args="manage" showMcpRedirectMessage />;
|
||||
if (process.env.USER_TYPE === 'ant') {
|
||||
return (
|
||||
<PluginSettings
|
||||
onComplete={onDone}
|
||||
args="manage"
|
||||
showMcpRedirectMessage
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <MCPSettings onComplete={onDone} />;
|
||||
|
||||
return <MCPSettings onComplete={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,75 +1,86 @@
|
||||
import { mkdir, writeFile } from 'fs/promises';
|
||||
import * as React from 'react';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { MemoryFileSelector } from '../../components/memory/MemoryFileSelector.js';
|
||||
import { getRelativeMemoryPath } from '../../components/memory/MemoryUpdateNotification.js';
|
||||
import { Box, Link, Text } from '../../ink.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import { clearMemoryFileCaches, getMemoryFiles } from '../../utils/claudemd.js';
|
||||
import { getClaudeConfigHomeDir } from '../../utils/envUtils.js';
|
||||
import { getErrnoCode } from '../../utils/errors.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import { editFileInEditor } from '../../utils/promptEditor.js';
|
||||
import { mkdir, writeFile } from 'fs/promises'
|
||||
import * as React from 'react'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { MemoryFileSelector } from '../../components/memory/MemoryFileSelector.js'
|
||||
import { getRelativeMemoryPath } from '../../components/memory/MemoryUpdateNotification.js'
|
||||
import { Box, Link, Text } from '../../ink.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
import { clearMemoryFileCaches, getMemoryFiles } from '../../utils/claudemd.js'
|
||||
import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
|
||||
import { getErrnoCode } from '../../utils/errors.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import { editFileInEditor } from '../../utils/promptEditor.js'
|
||||
|
||||
function MemoryCommand({
|
||||
onDone
|
||||
onDone,
|
||||
}: {
|
||||
onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
}): React.ReactNode {
|
||||
const handleSelectMemoryFile = async (memoryPath: string) => {
|
||||
try {
|
||||
// Create claude directory if it doesn't exist (idempotent with recursive)
|
||||
if (memoryPath.includes(getClaudeConfigHomeDir())) {
|
||||
await mkdir(getClaudeConfigHomeDir(), {
|
||||
recursive: true
|
||||
});
|
||||
await mkdir(getClaudeConfigHomeDir(), { recursive: true })
|
||||
}
|
||||
|
||||
// Create file if it doesn't exist (wx flag fails if file exists,
|
||||
// which we catch to preserve existing content)
|
||||
try {
|
||||
await writeFile(memoryPath, '', {
|
||||
encoding: 'utf8',
|
||||
flag: 'wx'
|
||||
});
|
||||
await writeFile(memoryPath, '', { encoding: 'utf8', flag: 'wx' })
|
||||
} catch (e: unknown) {
|
||||
if (getErrnoCode(e) !== 'EEXIST') {
|
||||
throw e;
|
||||
throw e
|
||||
}
|
||||
}
|
||||
await editFileInEditor(memoryPath);
|
||||
|
||||
await editFileInEditor(memoryPath)
|
||||
|
||||
// Determine which environment variable controls the editor
|
||||
let editorSource = 'default';
|
||||
let editorValue = '';
|
||||
let editorSource = 'default'
|
||||
let editorValue = ''
|
||||
if (process.env.VISUAL) {
|
||||
editorSource = '$VISUAL';
|
||||
editorValue = process.env.VISUAL;
|
||||
editorSource = '$VISUAL'
|
||||
editorValue = process.env.VISUAL
|
||||
} else if (process.env.EDITOR) {
|
||||
editorSource = '$EDITOR';
|
||||
editorValue = process.env.EDITOR;
|
||||
editorSource = '$EDITOR'
|
||||
editorValue = process.env.EDITOR
|
||||
}
|
||||
const editorInfo = editorSource !== 'default' ? `Using ${editorSource}="${editorValue}".` : '';
|
||||
const editorHint = editorInfo ? `> ${editorInfo} To change editor, set $EDITOR or $VISUAL environment variable.` : `> To use a different editor, set the $EDITOR or $VISUAL environment variable.`;
|
||||
onDone(`Opened memory file at ${getRelativeMemoryPath(memoryPath)}\n\n${editorHint}`, {
|
||||
display: 'system'
|
||||
});
|
||||
|
||||
const editorInfo =
|
||||
editorSource !== 'default'
|
||||
? `Using ${editorSource}="${editorValue}".`
|
||||
: ''
|
||||
|
||||
const editorHint = editorInfo
|
||||
? `> ${editorInfo} To change editor, set $EDITOR or $VISUAL environment variable.`
|
||||
: `> To use a different editor, set the $EDITOR or $VISUAL environment variable.`
|
||||
|
||||
onDone(
|
||||
`Opened memory file at ${getRelativeMemoryPath(memoryPath)}\n\n${editorHint}`,
|
||||
{ display: 'system' },
|
||||
)
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
onDone(`Error opening memory file: ${error}`);
|
||||
logError(error)
|
||||
onDone(`Error opening memory file: ${error}`)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
onDone('Cancelled memory editing', {
|
||||
display: 'system'
|
||||
});
|
||||
};
|
||||
return <Dialog title="Memory" onCancel={handleCancel} color="remember">
|
||||
onDone('Cancelled memory editing', { display: 'system' })
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title="Memory" onCancel={handleCancel} color="remember">
|
||||
<Box flexDirection="column">
|
||||
<React.Suspense fallback={null}>
|
||||
<MemoryFileSelector onSelect={handleSelectMemoryFile} onCancel={handleCancel} />
|
||||
<MemoryFileSelector
|
||||
onSelect={handleSelectMemoryFile}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</React.Suspense>
|
||||
|
||||
<Box marginTop={1}>
|
||||
@@ -78,12 +89,14 @@ function MemoryCommand({
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Dialog>;
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export const call: LocalJSXCommandCall = async onDone => {
|
||||
// Clear + prime before rendering — Suspense handles the unprimed case,
|
||||
// but awaiting here avoids a fallback flash on initial open.
|
||||
clearMemoryFileCaches();
|
||||
await getMemoryFiles();
|
||||
return <MemoryCommand onDone={onDone} />;
|
||||
};
|
||||
clearMemoryFileCaches()
|
||||
await getMemoryFiles()
|
||||
return <MemoryCommand onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,273 +1,117 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { toString as qrToString } from 'qrcode';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Pane } from '../../components/design-system/Pane.js';
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
type Platform = 'ios' | 'android';
|
||||
import { toString as qrToString } from 'qrcode'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Pane } from '../../components/design-system/Pane.js'
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
|
||||
type Platform = 'ios' | 'android'
|
||||
|
||||
type Props = {
|
||||
onDone: () => void;
|
||||
};
|
||||
const PLATFORMS: Record<Platform, {
|
||||
url: string;
|
||||
}> = {
|
||||
onDone: () => void
|
||||
}
|
||||
|
||||
const PLATFORMS: Record<Platform, { url: string }> = {
|
||||
ios: {
|
||||
url: 'https://apps.apple.com/app/claude-by-anthropic/id6473753684'
|
||||
url: 'https://apps.apple.com/app/claude-by-anthropic/id6473753684',
|
||||
},
|
||||
android: {
|
||||
url: 'https://play.google.com/store/apps/details?id=com.anthropic.claude'
|
||||
}
|
||||
};
|
||||
function MobileQRCode(t0) {
|
||||
const $ = _c(52);
|
||||
const {
|
||||
onDone
|
||||
} = t0;
|
||||
const [platform, setPlatform] = useState("ios");
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = {
|
||||
ios: "",
|
||||
android: ""
|
||||
};
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const [qrCodes, setQrCodes] = useState(t1);
|
||||
const {
|
||||
url
|
||||
} = PLATFORMS[platform];
|
||||
const qrCode = qrCodes[platform];
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = () => {
|
||||
const generateQRCodes = async function generateQRCodes() {
|
||||
const [ios, android] = await Promise.all([qrToString(PLATFORMS.ios.url, {
|
||||
type: "utf8",
|
||||
errorCorrectionLevel: "L"
|
||||
}), qrToString(PLATFORMS.android.url, {
|
||||
type: "utf8",
|
||||
errorCorrectionLevel: "L"
|
||||
})]);
|
||||
setQrCodes({
|
||||
ios,
|
||||
android
|
||||
});
|
||||
};
|
||||
generateQRCodes().catch(_temp);
|
||||
};
|
||||
t3 = [];
|
||||
$[1] = t2;
|
||||
$[2] = t3;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
t3 = $[2];
|
||||
}
|
||||
useEffect(t2, t3);
|
||||
let t4;
|
||||
if ($[3] !== onDone) {
|
||||
t4 = () => {
|
||||
onDone();
|
||||
};
|
||||
$[3] = onDone;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
const handleClose = t4;
|
||||
let t5;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[5] = t5;
|
||||
} else {
|
||||
t5 = $[5];
|
||||
}
|
||||
useKeybinding("confirm:no", handleClose, t5);
|
||||
let t6;
|
||||
if ($[6] !== onDone) {
|
||||
t6 = function handleKeyDown(e) {
|
||||
if (e.key === "q" || e.ctrl && e.key === "c") {
|
||||
e.preventDefault();
|
||||
onDone();
|
||||
return;
|
||||
}
|
||||
if (e.key === "tab" || e.key === "left" || e.key === "right") {
|
||||
e.preventDefault();
|
||||
setPlatform(_temp2);
|
||||
}
|
||||
};
|
||||
$[6] = onDone;
|
||||
$[7] = t6;
|
||||
} else {
|
||||
t6 = $[7];
|
||||
}
|
||||
const handleKeyDown = t6;
|
||||
let T0;
|
||||
let T1;
|
||||
let t10;
|
||||
let t11;
|
||||
let t12;
|
||||
let t13;
|
||||
let t7;
|
||||
let t8;
|
||||
let t9;
|
||||
if ($[8] !== handleKeyDown || $[9] !== qrCode) {
|
||||
const lines = qrCode.split("\n").filter(_temp3);
|
||||
T1 = Pane;
|
||||
T0 = Box;
|
||||
t7 = "column";
|
||||
t8 = 0;
|
||||
t9 = true;
|
||||
t10 = handleKeyDown;
|
||||
if ($[19] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t11 = <Text> </Text>;
|
||||
t12 = <Text> </Text>;
|
||||
$[19] = t11;
|
||||
$[20] = t12;
|
||||
} else {
|
||||
t11 = $[19];
|
||||
t12 = $[20];
|
||||
url: 'https://play.google.com/store/apps/details?id=com.anthropic.claude',
|
||||
},
|
||||
}
|
||||
|
||||
function MobileQRCode({ onDone }: Props): React.ReactNode {
|
||||
const [platform, setPlatform] = useState<Platform>('ios')
|
||||
const [qrCodes, setQrCodes] = useState<Record<Platform, string>>({
|
||||
ios: '',
|
||||
android: '',
|
||||
})
|
||||
|
||||
const { url } = PLATFORMS[platform]
|
||||
const qrCode = qrCodes[platform]
|
||||
|
||||
// Generate both QR codes upfront to avoid flicker when switching
|
||||
useEffect(() => {
|
||||
async function generateQRCodes(): Promise<void> {
|
||||
const [ios, android] = await Promise.all([
|
||||
qrToString(PLATFORMS.ios.url, {
|
||||
type: 'utf8',
|
||||
errorCorrectionLevel: 'L',
|
||||
}),
|
||||
qrToString(PLATFORMS.android.url, {
|
||||
type: 'utf8',
|
||||
errorCorrectionLevel: 'L',
|
||||
}),
|
||||
])
|
||||
setQrCodes({ ios, android })
|
||||
}
|
||||
generateQRCodes().catch(() => {
|
||||
// QR generation failed, leave empty
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
onDone()
|
||||
}, [onDone])
|
||||
|
||||
useKeybinding('confirm:no', handleClose, { context: 'Confirmation' })
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent): void {
|
||||
if (e.key === 'q' || (e.ctrl && e.key === 'c')) {
|
||||
e.preventDefault()
|
||||
onDone()
|
||||
return
|
||||
}
|
||||
if (e.key === 'tab' || e.key === 'left' || e.key === 'right') {
|
||||
e.preventDefault()
|
||||
setPlatform(prev => (prev === 'ios' ? 'android' : 'ios'))
|
||||
}
|
||||
t13 = lines.map(_temp4);
|
||||
$[8] = handleKeyDown;
|
||||
$[9] = qrCode;
|
||||
$[10] = T0;
|
||||
$[11] = T1;
|
||||
$[12] = t10;
|
||||
$[13] = t11;
|
||||
$[14] = t12;
|
||||
$[15] = t13;
|
||||
$[16] = t7;
|
||||
$[17] = t8;
|
||||
$[18] = t9;
|
||||
} else {
|
||||
T0 = $[10];
|
||||
T1 = $[11];
|
||||
t10 = $[12];
|
||||
t11 = $[13];
|
||||
t12 = $[14];
|
||||
t13 = $[15];
|
||||
t7 = $[16];
|
||||
t8 = $[17];
|
||||
t9 = $[18];
|
||||
}
|
||||
let t14;
|
||||
let t15;
|
||||
if ($[21] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t14 = <Text> </Text>;
|
||||
t15 = <Text> </Text>;
|
||||
$[21] = t14;
|
||||
$[22] = t15;
|
||||
} else {
|
||||
t14 = $[21];
|
||||
t15 = $[22];
|
||||
}
|
||||
const t16 = platform === "ios";
|
||||
const t17 = platform === "ios";
|
||||
let t18;
|
||||
if ($[23] !== t16 || $[24] !== t17) {
|
||||
t18 = <Text bold={t16} underline={t17}>iOS</Text>;
|
||||
$[23] = t16;
|
||||
$[24] = t17;
|
||||
$[25] = t18;
|
||||
} else {
|
||||
t18 = $[25];
|
||||
}
|
||||
let t19;
|
||||
if ($[26] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t19 = <Text dimColor={true}>{" / "}</Text>;
|
||||
$[26] = t19;
|
||||
} else {
|
||||
t19 = $[26];
|
||||
}
|
||||
const t20 = platform === "android";
|
||||
const t21 = platform === "android";
|
||||
let t22;
|
||||
if ($[27] !== t20 || $[28] !== t21) {
|
||||
t22 = <Text bold={t20} underline={t21}>Android</Text>;
|
||||
$[27] = t20;
|
||||
$[28] = t21;
|
||||
$[29] = t22;
|
||||
} else {
|
||||
t22 = $[29];
|
||||
}
|
||||
let t23;
|
||||
if ($[30] !== t18 || $[31] !== t22) {
|
||||
t23 = <Text>{t18}{t19}{t22}</Text>;
|
||||
$[30] = t18;
|
||||
$[31] = t22;
|
||||
$[32] = t23;
|
||||
} else {
|
||||
t23 = $[32];
|
||||
}
|
||||
let t24;
|
||||
if ($[33] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t24 = <Text dimColor={true}>(tab to switch, esc to close)</Text>;
|
||||
$[33] = t24;
|
||||
} else {
|
||||
t24 = $[33];
|
||||
}
|
||||
let t25;
|
||||
if ($[34] !== t23) {
|
||||
t25 = <Box flexDirection="row" gap={2}>{t23}{t24}</Box>;
|
||||
$[34] = t23;
|
||||
$[35] = t25;
|
||||
} else {
|
||||
t25 = $[35];
|
||||
}
|
||||
let t26;
|
||||
if ($[36] !== url) {
|
||||
t26 = <Text dimColor={true}>{url}</Text>;
|
||||
$[36] = url;
|
||||
$[37] = t26;
|
||||
} else {
|
||||
t26 = $[37];
|
||||
}
|
||||
let t27;
|
||||
if ($[38] !== T0 || $[39] !== t10 || $[40] !== t11 || $[41] !== t12 || $[42] !== t13 || $[43] !== t25 || $[44] !== t26 || $[45] !== t7 || $[46] !== t8 || $[47] !== t9) {
|
||||
t27 = <T0 flexDirection={t7} tabIndex={t8} autoFocus={t9} onKeyDown={t10}>{t11}{t12}{t13}{t14}{t15}{t25}{t26}</T0>;
|
||||
$[38] = T0;
|
||||
$[39] = t10;
|
||||
$[40] = t11;
|
||||
$[41] = t12;
|
||||
$[42] = t13;
|
||||
$[43] = t25;
|
||||
$[44] = t26;
|
||||
$[45] = t7;
|
||||
$[46] = t8;
|
||||
$[47] = t9;
|
||||
$[48] = t27;
|
||||
} else {
|
||||
t27 = $[48];
|
||||
}
|
||||
let t28;
|
||||
if ($[49] !== T1 || $[50] !== t27) {
|
||||
t28 = <T1>{t27}</T1>;
|
||||
$[49] = T1;
|
||||
$[50] = t27;
|
||||
$[51] = t28;
|
||||
} else {
|
||||
t28 = $[51];
|
||||
}
|
||||
return t28;
|
||||
|
||||
const lines = qrCode.split('\n').filter(line => line.length > 0)
|
||||
|
||||
return (
|
||||
<Pane>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Text> </Text>
|
||||
<Text> </Text>
|
||||
{lines.map((line, i) => (
|
||||
<Text key={i}>{line}</Text>
|
||||
))}
|
||||
<Text> </Text>
|
||||
<Text> </Text>
|
||||
|
||||
{/* Controls */}
|
||||
<Box flexDirection="row" gap={2}>
|
||||
<Text>
|
||||
<Text bold={platform === 'ios'} underline={platform === 'ios'}>
|
||||
iOS
|
||||
</Text>
|
||||
<Text dimColor>{' / '}</Text>
|
||||
<Text
|
||||
bold={platform === 'android'}
|
||||
underline={platform === 'android'}
|
||||
>
|
||||
Android
|
||||
</Text>
|
||||
</Text>
|
||||
<Text dimColor>(tab to switch, esc to close)</Text>
|
||||
</Box>
|
||||
<Text dimColor>{url}</Text>
|
||||
</Box>
|
||||
</Pane>
|
||||
)
|
||||
}
|
||||
function _temp4(line_0, i) {
|
||||
return <Text key={i}>{line_0}</Text>;
|
||||
}
|
||||
function _temp3(line) {
|
||||
return line.length > 0;
|
||||
}
|
||||
function _temp2(prev) {
|
||||
return prev === "ios" ? "android" : "ios";
|
||||
}
|
||||
function _temp() {}
|
||||
export async function call(onDone: LocalJSXCommandOnDone): Promise<React.ReactNode> {
|
||||
return <MobileQRCode onDone={onDone} />;
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
): Promise<React.ReactNode> {
|
||||
return <MobileQRCode onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,296 +1,337 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import chalk from 'chalk';
|
||||
import * as React from 'react';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import { ModelPicker } from '../../components/ModelPicker.js';
|
||||
import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.js';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js';
|
||||
import { useAppState, useSetAppState } from '../../state/AppState.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import type { EffortLevel } from '../../utils/effort.js';
|
||||
import { isBilledAsExtraUsage } from '../../utils/extraUsage.js';
|
||||
import { clearFastModeCooldown, isFastModeAvailable, isFastModeEnabled, isFastModeSupportedByModel } from '../../utils/fastMode.js';
|
||||
import { MODEL_ALIASES } from '../../utils/model/aliases.js';
|
||||
import { checkOpus1mAccess, checkSonnet1mAccess } from '../../utils/model/check1mAccess.js';
|
||||
import { getDefaultMainLoopModelSetting, isOpus1mMergeEnabled, renderDefaultModelSetting } from '../../utils/model/model.js';
|
||||
import { isModelAllowed } from '../../utils/model/modelAllowlist.js';
|
||||
import { validateModel } from '../../utils/model/validateModel.js';
|
||||
function ModelPickerWrapper(t0) {
|
||||
const $ = _c(17);
|
||||
const {
|
||||
onDone
|
||||
} = t0;
|
||||
const mainLoopModel = useAppState(_temp);
|
||||
const mainLoopModelForSession = useAppState(_temp2);
|
||||
const isFastMode = useAppState(_temp3);
|
||||
const setAppState = useSetAppState();
|
||||
let t1;
|
||||
if ($[0] !== mainLoopModel || $[1] !== onDone) {
|
||||
t1 = function handleCancel() {
|
||||
logEvent("tengu_model_command_menu", {
|
||||
action: "cancel" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
const displayModel = renderModelLabel(mainLoopModel);
|
||||
onDone(`Kept model as ${chalk.bold(displayModel)}`, {
|
||||
display: "system"
|
||||
});
|
||||
};
|
||||
$[0] = mainLoopModel;
|
||||
$[1] = onDone;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
import chalk from 'chalk'
|
||||
import * as React from 'react'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import { ModelPicker } from '../../components/ModelPicker.js'
|
||||
import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
import { useAppState, useSetAppState } from '../../state/AppState.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
import type { EffortLevel } from '../../utils/effort.js'
|
||||
import { isBilledAsExtraUsage } from '../../utils/extraUsage.js'
|
||||
import {
|
||||
clearFastModeCooldown,
|
||||
isFastModeAvailable,
|
||||
isFastModeEnabled,
|
||||
isFastModeSupportedByModel,
|
||||
} from '../../utils/fastMode.js'
|
||||
import { MODEL_ALIASES } from '../../utils/model/aliases.js'
|
||||
import {
|
||||
checkOpus1mAccess,
|
||||
checkSonnet1mAccess,
|
||||
} from '../../utils/model/check1mAccess.js'
|
||||
import {
|
||||
getDefaultMainLoopModelSetting,
|
||||
isOpus1mMergeEnabled,
|
||||
renderDefaultModelSetting,
|
||||
} from '../../utils/model/model.js'
|
||||
import { isModelAllowed } from '../../utils/model/modelAllowlist.js'
|
||||
import { validateModel } from '../../utils/model/validateModel.js'
|
||||
|
||||
function ModelPickerWrapper({
|
||||
onDone,
|
||||
}: {
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
}): React.ReactNode {
|
||||
const mainLoopModel = useAppState(s => s.mainLoopModel)
|
||||
const mainLoopModelForSession = useAppState(s => s.mainLoopModelForSession)
|
||||
const isFastMode = useAppState(s => s.fastMode)
|
||||
const setAppState = useSetAppState()
|
||||
|
||||
function handleCancel(): void {
|
||||
logEvent('tengu_model_command_menu', {
|
||||
action:
|
||||
'cancel' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
const displayModel = renderModelLabel(mainLoopModel)
|
||||
onDone(`Kept model as ${chalk.bold(displayModel)}`, {
|
||||
display: 'system',
|
||||
})
|
||||
}
|
||||
const handleCancel = t1;
|
||||
let t2;
|
||||
if ($[3] !== isFastMode || $[4] !== mainLoopModel || $[5] !== onDone || $[6] !== setAppState) {
|
||||
t2 = function handleSelect(model, effort) {
|
||||
logEvent("tengu_model_command_menu", {
|
||||
action: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
from_model: mainLoopModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
to_model: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
mainLoopModel: model,
|
||||
mainLoopModelForSession: null
|
||||
}));
|
||||
let message = `Set model to ${chalk.bold(renderModelLabel(model))}`;
|
||||
if (effort !== undefined) {
|
||||
message = message + ` with ${chalk.bold(effort)} effort`;
|
||||
|
||||
function handleSelect(
|
||||
model: string | null,
|
||||
effort: EffortLevel | undefined,
|
||||
): void {
|
||||
logEvent('tengu_model_command_menu', {
|
||||
action:
|
||||
model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
from_model:
|
||||
mainLoopModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
to_model:
|
||||
model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
mainLoopModel: model,
|
||||
mainLoopModelForSession: null,
|
||||
}))
|
||||
|
||||
let message = `Set model to ${chalk.bold(renderModelLabel(model))}`
|
||||
if (effort !== undefined) {
|
||||
message += ` with ${chalk.bold(effort)} effort`
|
||||
}
|
||||
|
||||
// Turn off fast mode if switching to unsupported model
|
||||
let wasFastModeToggledOn = undefined
|
||||
if (isFastModeEnabled()) {
|
||||
clearFastModeCooldown()
|
||||
if (!isFastModeSupportedByModel(model) && isFastMode) {
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
fastMode: false,
|
||||
}))
|
||||
wasFastModeToggledOn = false
|
||||
// Do not update fast mode in settings since this is an automatic downgrade
|
||||
} else if (
|
||||
isFastModeSupportedByModel(model) &&
|
||||
isFastModeAvailable() &&
|
||||
isFastMode
|
||||
) {
|
||||
message += ` · Fast mode ON`
|
||||
wasFastModeToggledOn = true
|
||||
}
|
||||
let wasFastModeToggledOn = undefined;
|
||||
if (isFastModeEnabled()) {
|
||||
clearFastModeCooldown();
|
||||
if (!isFastModeSupportedByModel(model) && isFastMode) {
|
||||
setAppState(_temp4);
|
||||
wasFastModeToggledOn = false;
|
||||
} else {
|
||||
if (isFastModeSupportedByModel(model) && isFastModeAvailable() && isFastMode) {
|
||||
message = message + " \xB7 Fast mode ON";
|
||||
wasFastModeToggledOn = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isBilledAsExtraUsage(model, wasFastModeToggledOn === true, isOpus1mMergeEnabled())) {
|
||||
message = message + " \xB7 Billed as extra usage";
|
||||
}
|
||||
if (wasFastModeToggledOn === false) {
|
||||
message = message + " \xB7 Fast mode OFF";
|
||||
}
|
||||
onDone(message);
|
||||
};
|
||||
$[3] = isFastMode;
|
||||
$[4] = mainLoopModel;
|
||||
$[5] = onDone;
|
||||
$[6] = setAppState;
|
||||
$[7] = t2;
|
||||
} else {
|
||||
t2 = $[7];
|
||||
}
|
||||
|
||||
if (
|
||||
isBilledAsExtraUsage(
|
||||
model,
|
||||
wasFastModeToggledOn === true,
|
||||
isOpus1mMergeEnabled(),
|
||||
)
|
||||
) {
|
||||
message += ` · Billed as extra usage`
|
||||
}
|
||||
|
||||
if (wasFastModeToggledOn === false) {
|
||||
// Fast mode was toggled off, show suffix after extra usage billing
|
||||
message += ` · Fast mode OFF`
|
||||
}
|
||||
|
||||
onDone(message)
|
||||
}
|
||||
const handleSelect = t2;
|
||||
let t3;
|
||||
if ($[8] !== isFastMode || $[9] !== mainLoopModel) {
|
||||
t3 = isFastModeEnabled() && isFastMode && isFastModeSupportedByModel(mainLoopModel) && isFastModeAvailable();
|
||||
$[8] = isFastMode;
|
||||
$[9] = mainLoopModel;
|
||||
$[10] = t3;
|
||||
} else {
|
||||
t3 = $[10];
|
||||
}
|
||||
let t4;
|
||||
if ($[11] !== handleCancel || $[12] !== handleSelect || $[13] !== mainLoopModel || $[14] !== mainLoopModelForSession || $[15] !== t3) {
|
||||
t4 = <ModelPicker initial={mainLoopModel} sessionModel={mainLoopModelForSession} onSelect={handleSelect} onCancel={handleCancel} isStandaloneCommand={true} showFastModeNotice={t3} />;
|
||||
$[11] = handleCancel;
|
||||
$[12] = handleSelect;
|
||||
$[13] = mainLoopModel;
|
||||
$[14] = mainLoopModelForSession;
|
||||
$[15] = t3;
|
||||
$[16] = t4;
|
||||
} else {
|
||||
t4 = $[16];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
function _temp4(prev_0) {
|
||||
return {
|
||||
...prev_0,
|
||||
fastMode: false
|
||||
};
|
||||
}
|
||||
function _temp3(s_1) {
|
||||
return s_1.fastMode;
|
||||
}
|
||||
function _temp2(s_0) {
|
||||
return s_0.mainLoopModelForSession;
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.mainLoopModel;
|
||||
|
||||
return (
|
||||
<ModelPicker
|
||||
initial={mainLoopModel}
|
||||
sessionModel={mainLoopModelForSession}
|
||||
onSelect={handleSelect}
|
||||
onCancel={handleCancel}
|
||||
isStandaloneCommand
|
||||
showFastModeNotice={
|
||||
isFastModeEnabled() &&
|
||||
isFastMode &&
|
||||
isFastModeSupportedByModel(mainLoopModel) &&
|
||||
isFastModeAvailable()
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SetModelAndClose({
|
||||
args,
|
||||
onDone
|
||||
onDone,
|
||||
}: {
|
||||
args: string;
|
||||
onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
args: string
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
}): React.ReactNode {
|
||||
const isFastMode = useAppState(s => s.fastMode);
|
||||
const setAppState = useSetAppState();
|
||||
const model = args === 'default' ? null : args;
|
||||
const isFastMode = useAppState(s => s.fastMode)
|
||||
const setAppState = useSetAppState()
|
||||
const model = args === 'default' ? null : args
|
||||
|
||||
React.useEffect(() => {
|
||||
async function handleModelChange(): Promise<void> {
|
||||
if (model && !isModelAllowed(model)) {
|
||||
onDone(`Model '${model}' is not available. Your organization restricts model selection.`, {
|
||||
display: 'system'
|
||||
});
|
||||
return;
|
||||
onDone(
|
||||
`Model '${model}' is not available. Your organization restricts model selection.`,
|
||||
{ display: 'system' },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// @[MODEL LAUNCH]: Update check for 1M access.
|
||||
if (model && isOpus1mUnavailable(model)) {
|
||||
onDone(`Opus 4.6 with 1M context is not available for your account. Learn more: https://code.claude.com/docs/en/model-config#extended-context-with-1m`, {
|
||||
display: 'system'
|
||||
});
|
||||
return;
|
||||
onDone(
|
||||
`Opus 4.6 with 1M context is not available for your account. Learn more: https://code.claude.com/docs/en/model-config#extended-context-with-1m`,
|
||||
{ display: 'system' },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (model && isSonnet1mUnavailable(model)) {
|
||||
onDone(`Sonnet 4.6 with 1M context is not available for your account. Learn more: https://code.claude.com/docs/en/model-config#extended-context-with-1m`, {
|
||||
display: 'system'
|
||||
});
|
||||
return;
|
||||
onDone(
|
||||
`Sonnet 4.6 with 1M context is not available for your account. Learn more: https://code.claude.com/docs/en/model-config#extended-context-with-1m`,
|
||||
{ display: 'system' },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Skip validation for default model
|
||||
if (!model) {
|
||||
setModel(null);
|
||||
return;
|
||||
setModel(null)
|
||||
return
|
||||
}
|
||||
|
||||
// Skip validation for known aliases - they're predefined and should work
|
||||
if (isKnownAlias(model)) {
|
||||
setModel(model);
|
||||
return;
|
||||
setModel(model)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate and set custom model
|
||||
try {
|
||||
// Don't use parseUserSpecifiedModel for non-aliases since it lowercases the input
|
||||
// and model names are case-sensitive
|
||||
const {
|
||||
valid,
|
||||
error: error_0
|
||||
} = await validateModel(model);
|
||||
const { valid, error } = await validateModel(model)
|
||||
|
||||
if (valid) {
|
||||
setModel(model);
|
||||
setModel(model)
|
||||
} else {
|
||||
onDone(error_0 || `Model '${model}' not found`, {
|
||||
display: 'system'
|
||||
});
|
||||
onDone(error || `Model '${model}' not found`, {
|
||||
display: 'system',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
onDone(`Failed to validate model: ${(error as Error).message}`, {
|
||||
display: 'system'
|
||||
});
|
||||
display: 'system',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function setModel(modelValue: string | null): void {
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
mainLoopModel: modelValue,
|
||||
mainLoopModelForSession: null
|
||||
}));
|
||||
let message = `Set model to ${chalk.bold(renderModelLabel(modelValue))}`;
|
||||
let wasFastModeToggledOn = undefined;
|
||||
mainLoopModelForSession: null,
|
||||
}))
|
||||
let message = `Set model to ${chalk.bold(renderModelLabel(modelValue))}`
|
||||
|
||||
let wasFastModeToggledOn = undefined
|
||||
if (isFastModeEnabled()) {
|
||||
clearFastModeCooldown();
|
||||
clearFastModeCooldown()
|
||||
if (!isFastModeSupportedByModel(modelValue) && isFastMode) {
|
||||
setAppState(prev_0 => ({
|
||||
...prev_0,
|
||||
fastMode: false
|
||||
}));
|
||||
wasFastModeToggledOn = false;
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
fastMode: false,
|
||||
}))
|
||||
wasFastModeToggledOn = false
|
||||
// Do not update fast mode in settings since this is an automatic downgrade
|
||||
} else if (isFastModeSupportedByModel(modelValue) && isFastMode) {
|
||||
message += ` · Fast mode ON`;
|
||||
wasFastModeToggledOn = true;
|
||||
message += ` · Fast mode ON`
|
||||
wasFastModeToggledOn = true
|
||||
}
|
||||
}
|
||||
if (isBilledAsExtraUsage(modelValue, wasFastModeToggledOn === true, isOpus1mMergeEnabled())) {
|
||||
message += ` · Billed as extra usage`;
|
||||
|
||||
if (
|
||||
isBilledAsExtraUsage(
|
||||
modelValue,
|
||||
wasFastModeToggledOn === true,
|
||||
isOpus1mMergeEnabled(),
|
||||
)
|
||||
) {
|
||||
message += ` · Billed as extra usage`
|
||||
}
|
||||
|
||||
if (wasFastModeToggledOn === false) {
|
||||
// Fast mode was toggled off, show suffix after extra usage billing
|
||||
message += ` · Fast mode OFF`;
|
||||
message += ` · Fast mode OFF`
|
||||
}
|
||||
onDone(message);
|
||||
|
||||
onDone(message)
|
||||
}
|
||||
void handleModelChange();
|
||||
}, [model, onDone, setAppState]);
|
||||
return null;
|
||||
|
||||
void handleModelChange()
|
||||
}, [model, onDone, setAppState])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function isKnownAlias(model: string): boolean {
|
||||
return (MODEL_ALIASES as readonly string[]).includes(model.toLowerCase().trim());
|
||||
return (MODEL_ALIASES as readonly string[]).includes(
|
||||
model.toLowerCase().trim(),
|
||||
)
|
||||
}
|
||||
|
||||
function isOpus1mUnavailable(model: string): boolean {
|
||||
const m = model.toLowerCase();
|
||||
return !checkOpus1mAccess() && !isOpus1mMergeEnabled() && m.includes('opus') && m.includes('[1m]');
|
||||
const m = model.toLowerCase()
|
||||
return (
|
||||
!checkOpus1mAccess() &&
|
||||
!isOpus1mMergeEnabled() &&
|
||||
m.includes('opus') &&
|
||||
m.includes('[1m]')
|
||||
)
|
||||
}
|
||||
|
||||
function isSonnet1mUnavailable(model: string): boolean {
|
||||
const m = model.toLowerCase();
|
||||
const m = model.toLowerCase()
|
||||
// Warn about Sonnet and Sonnet 4.6, but not Sonnet 4.5 since that had
|
||||
// a different access criteria.
|
||||
return !checkSonnet1mAccess() && (m.includes('sonnet[1m]') || m.includes('sonnet-4-6[1m]'));
|
||||
return (
|
||||
!checkSonnet1mAccess() &&
|
||||
(m.includes('sonnet[1m]') || m.includes('sonnet-4-6[1m]'))
|
||||
)
|
||||
}
|
||||
function ShowModelAndClose(t0) {
|
||||
const {
|
||||
onDone
|
||||
} = t0;
|
||||
const mainLoopModel = useAppState(_temp7);
|
||||
const mainLoopModelForSession = useAppState(_temp8);
|
||||
const effortValue = useAppState(_temp9);
|
||||
const displayModel = renderModelLabel(mainLoopModel);
|
||||
const effortInfo = effortValue !== undefined ? ` (effort: ${effortValue})` : "";
|
||||
|
||||
function ShowModelAndClose({
|
||||
onDone,
|
||||
}: {
|
||||
onDone: (result?: string) => void
|
||||
}): React.ReactNode {
|
||||
const mainLoopModel = useAppState(s => s.mainLoopModel)
|
||||
const mainLoopModelForSession = useAppState(s => s.mainLoopModelForSession)
|
||||
const effortValue = useAppState(s => s.effortValue)
|
||||
const displayModel = renderModelLabel(mainLoopModel)
|
||||
const effortInfo =
|
||||
effortValue !== undefined ? ` (effort: ${effortValue})` : ''
|
||||
|
||||
if (mainLoopModelForSession) {
|
||||
onDone(`Current model: ${chalk.bold(renderModelLabel(mainLoopModelForSession))} (session override from plan mode)\nBase model: ${displayModel}${effortInfo}`);
|
||||
onDone(
|
||||
`Current model: ${chalk.bold(renderModelLabel(mainLoopModelForSession))} (session override from plan mode)\nBase model: ${displayModel}${effortInfo}`,
|
||||
)
|
||||
} else {
|
||||
onDone(`Current model: ${displayModel}${effortInfo}`);
|
||||
onDone(`Current model: ${displayModel}${effortInfo}`)
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function _temp9(s_1) {
|
||||
return s_1.effortValue;
|
||||
}
|
||||
function _temp8(s_0) {
|
||||
return s_0.mainLoopModelForSession;
|
||||
}
|
||||
function _temp7(s) {
|
||||
return s.mainLoopModel;
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, _context, args) => {
|
||||
args = args?.trim() || '';
|
||||
args = args?.trim() || ''
|
||||
if (COMMON_INFO_ARGS.includes(args)) {
|
||||
logEvent('tengu_model_command_inline_help', {
|
||||
args: args as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
return <ShowModelAndClose onDone={onDone} />;
|
||||
args: args as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
return <ShowModelAndClose onDone={onDone} />
|
||||
}
|
||||
if (COMMON_HELP_ARGS.includes(args)) {
|
||||
onDone('Run /model to open the model selection menu, or /model [modelName] to set the model.', {
|
||||
display: 'system'
|
||||
});
|
||||
return;
|
||||
onDone(
|
||||
'Run /model to open the model selection menu, or /model [modelName] to set the model.',
|
||||
{ display: 'system' },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (args) {
|
||||
logEvent('tengu_model_command_inline', {
|
||||
args: args as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
return <SetModelAndClose args={args} onDone={onDone} />;
|
||||
args: args as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
return <SetModelAndClose args={args} onDone={onDone} />
|
||||
}
|
||||
return <ModelPickerWrapper onDone={onDone} />;
|
||||
};
|
||||
function renderModelLabel(model: string | null): string {
|
||||
const rendered = renderDefaultModelSetting(model ?? getDefaultMainLoopModelSetting());
|
||||
return model === null ? `${rendered} (default)` : rendered;
|
||||
|
||||
return <ModelPickerWrapper onDone={onDone} />
|
||||
}
|
||||
|
||||
function renderModelLabel(model: string | null): string {
|
||||
const rendered = renderDefaultModelSetting(
|
||||
model ?? getDefaultMainLoopModelSetting(),
|
||||
)
|
||||
return model === null ? `${rendered} (default)` : rendered
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
|
||||
export async function call(onDone: LocalJSXCommandOnDone): Promise<undefined> {
|
||||
onDone('/output-style has been deprecated. Use /config to change your output style, or set it in your settings file. Changes take effect on the next session.', {
|
||||
display: 'system'
|
||||
});
|
||||
onDone(
|
||||
'/output-style has been deprecated. Use /config to change your output style, or set it in your settings file. Changes take effect on the next session.',
|
||||
{ display: 'system' },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import * as React from 'react';
|
||||
import { Passes } from '../../components/Passes/Passes.js';
|
||||
import { logEvent } from '../../services/analytics/index.js';
|
||||
import { getCachedRemainingPasses } from '../../services/api/referral.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
|
||||
export async function call(onDone: LocalJSXCommandOnDone): Promise<React.ReactNode> {
|
||||
import * as React from 'react'
|
||||
import { Passes } from '../../components/Passes/Passes.js'
|
||||
import { logEvent } from '../../services/analytics/index.js'
|
||||
import { getCachedRemainingPasses } from '../../services/api/referral.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
): Promise<React.ReactNode> {
|
||||
// Mark that user has visited /passes so we stop showing the upsell
|
||||
const config = getGlobalConfig();
|
||||
const isFirstVisit = !config.hasVisitedPasses;
|
||||
const config = getGlobalConfig()
|
||||
const isFirstVisit = !config.hasVisitedPasses
|
||||
if (isFirstVisit) {
|
||||
const remaining = getCachedRemainingPasses();
|
||||
const remaining = getCachedRemainingPasses()
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
hasVisitedPasses: true,
|
||||
passesLastSeenRemaining: remaining ?? current.passesLastSeenRemaining
|
||||
}));
|
||||
passesLastSeenRemaining: remaining ?? current.passesLastSeenRemaining,
|
||||
}))
|
||||
}
|
||||
logEvent('tengu_guest_passes_visited', {
|
||||
is_first_visit: isFirstVisit
|
||||
});
|
||||
return <Passes onDone={onDone} />;
|
||||
logEvent('tengu_guest_passes_visited', { is_first_visit: isFirstVisit })
|
||||
return <Passes onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import { PermissionRuleList } from '../../components/permissions/rules/PermissionRuleList.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import { createPermissionRetryMessage } from '../../utils/messages.js';
|
||||
import * as React from 'react'
|
||||
import { PermissionRuleList } from '../../components/permissions/rules/PermissionRuleList.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
import { createPermissionRetryMessage } from '../../utils/messages.js'
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, context) => {
|
||||
return <PermissionRuleList onExit={onDone} onRetryDenials={commands => {
|
||||
context.setMessages(prev => [...prev, createPermissionRetryMessage(commands)]);
|
||||
}} />;
|
||||
};
|
||||
return (
|
||||
<PermissionRuleList
|
||||
onExit={onDone}
|
||||
onRetryDenials={commands => {
|
||||
context.setMessages(prev => [
|
||||
...prev,
|
||||
createPermissionRetryMessage(commands),
|
||||
])
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,121 +1,107 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { handlePlanModeTransition } from '../../bootstrap/state.js';
|
||||
import type { LocalJSXCommandContext } from '../../commands.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { getExternalEditor } from '../../utils/editor.js';
|
||||
import { toIDEDisplayName } from '../../utils/ide.js';
|
||||
import { applyPermissionUpdate } from '../../utils/permissions/PermissionUpdate.js';
|
||||
import { prepareContextForPlanMode } from '../../utils/permissions/permissionSetup.js';
|
||||
import { getPlan, getPlanFilePath } from '../../utils/plans.js';
|
||||
import { editFileInEditor } from '../../utils/promptEditor.js';
|
||||
import { renderToString } from '../../utils/staticRender.js';
|
||||
function PlanDisplay(t0) {
|
||||
const $ = _c(11);
|
||||
const {
|
||||
planContent,
|
||||
planPath,
|
||||
editorName
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text bold={true}>Current Plan</Text>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
let t2;
|
||||
if ($[1] !== planPath) {
|
||||
t2 = <Text dimColor={true}>{planPath}</Text>;
|
||||
$[1] = planPath;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== planContent) {
|
||||
t3 = <Box marginTop={1}><Text>{planContent}</Text></Box>;
|
||||
$[3] = planContent;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== editorName) {
|
||||
t4 = editorName && <Box marginTop={1}><Text dimColor={true}>"/plan open"</Text><Text dimColor={true}> to edit this plan in </Text><Text bold={true} dimColor={true}>{editorName}</Text></Box>;
|
||||
$[5] = editorName;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
let t5;
|
||||
if ($[7] !== t2 || $[8] !== t3 || $[9] !== t4) {
|
||||
t5 = <Box flexDirection="column">{t1}{t2}{t3}{t4}</Box>;
|
||||
$[7] = t2;
|
||||
$[8] = t3;
|
||||
$[9] = t4;
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
}
|
||||
return t5;
|
||||
import * as React from 'react'
|
||||
import { handlePlanModeTransition } from '../../bootstrap/state.js'
|
||||
import type { LocalJSXCommandContext } from '../../commands.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import { getExternalEditor } from '../../utils/editor.js'
|
||||
import { toIDEDisplayName } from '../../utils/ide.js'
|
||||
import { applyPermissionUpdate } from '../../utils/permissions/PermissionUpdate.js'
|
||||
import { prepareContextForPlanMode } from '../../utils/permissions/permissionSetup.js'
|
||||
import { getPlan, getPlanFilePath } from '../../utils/plans.js'
|
||||
import { editFileInEditor } from '../../utils/promptEditor.js'
|
||||
import { renderToString } from '../../utils/staticRender.js'
|
||||
|
||||
function PlanDisplay({
|
||||
planContent,
|
||||
planPath,
|
||||
editorName,
|
||||
}: {
|
||||
planContent: string
|
||||
planPath: string
|
||||
editorName: string | undefined
|
||||
}): React.ReactNode {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text bold>Current Plan</Text>
|
||||
<Text dimColor>{planPath}</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text>{planContent}</Text>
|
||||
</Box>
|
||||
{editorName && (
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>"/plan open"</Text>
|
||||
<Text dimColor> to edit this plan in </Text>
|
||||
<Text bold dimColor>
|
||||
{editorName}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext, args: string): Promise<React.ReactNode> {
|
||||
const {
|
||||
getAppState,
|
||||
setAppState
|
||||
} = context;
|
||||
const appState = getAppState();
|
||||
const currentMode = appState.toolPermissionContext.mode;
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
args: string,
|
||||
): Promise<React.ReactNode> {
|
||||
const { getAppState, setAppState } = context
|
||||
const appState = getAppState()
|
||||
const currentMode = appState.toolPermissionContext.mode
|
||||
|
||||
// If not in plan mode, enable it
|
||||
if (currentMode !== 'plan') {
|
||||
handlePlanModeTransition(currentMode, 'plan');
|
||||
handlePlanModeTransition(currentMode, 'plan')
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
toolPermissionContext: applyPermissionUpdate(prepareContextForPlanMode(prev.toolPermissionContext), {
|
||||
type: 'setMode',
|
||||
mode: 'plan',
|
||||
destination: 'session'
|
||||
})
|
||||
}));
|
||||
const description = args.trim();
|
||||
toolPermissionContext: applyPermissionUpdate(
|
||||
prepareContextForPlanMode(prev.toolPermissionContext),
|
||||
{ type: 'setMode', mode: 'plan', destination: 'session' },
|
||||
),
|
||||
}))
|
||||
const description = args.trim()
|
||||
if (description && description !== 'open') {
|
||||
onDone('Enabled plan mode', {
|
||||
shouldQuery: true
|
||||
});
|
||||
onDone('Enabled plan mode', { shouldQuery: true })
|
||||
} else {
|
||||
onDone('Enabled plan mode');
|
||||
onDone('Enabled plan mode')
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Already in plan mode - show the current plan
|
||||
const planContent = getPlan();
|
||||
const planPath = getPlanFilePath();
|
||||
const planContent = getPlan()
|
||||
const planPath = getPlanFilePath()
|
||||
|
||||
if (!planContent) {
|
||||
onDone('Already in plan mode. No plan written yet.');
|
||||
return null;
|
||||
onDone('Already in plan mode. No plan written yet.')
|
||||
return null
|
||||
}
|
||||
|
||||
// If user typed "/plan open", open in editor
|
||||
const argList = args.trim().split(/\s+/);
|
||||
const argList = args.trim().split(/\s+/)
|
||||
if (argList[0] === 'open') {
|
||||
const result = await editFileInEditor(planPath);
|
||||
const result = await editFileInEditor(planPath)
|
||||
if (result.error) {
|
||||
onDone(`Failed to open plan in editor: ${result.error}`);
|
||||
onDone(`Failed to open plan in editor: ${result.error}`)
|
||||
} else {
|
||||
onDone(`Opened plan in editor: ${planPath}`);
|
||||
onDone(`Opened plan in editor: ${planPath}`)
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const editor = getExternalEditor();
|
||||
const editorName = editor ? toIDEDisplayName(editor) : undefined;
|
||||
const display = <PlanDisplay planContent={planContent} planPath={planPath} editorName={editorName} />;
|
||||
|
||||
const editor = getExternalEditor()
|
||||
const editorName = editor ? toIDEDisplayName(editor) : undefined
|
||||
|
||||
const display = (
|
||||
<PlanDisplay
|
||||
planContent={planContent}
|
||||
planPath={planPath}
|
||||
editorName={editorName}
|
||||
/>
|
||||
)
|
||||
|
||||
// Render to string and pass to onDone like local commands do
|
||||
const output = await renderToString(display);
|
||||
onDone(output);
|
||||
return null;
|
||||
const output = await renderToString(display)
|
||||
onDone(output)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,31 +1,39 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
|
||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js';
|
||||
import { Byline } from '../../components/design-system/Byline.js';
|
||||
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js';
|
||||
import { Spinner } from '../../components/Spinner.js';
|
||||
import TextInput from '../../components/TextInput.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { toError } from '../../utils/errors.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import { clearAllCaches } from '../../utils/plugins/cacheUtils.js';
|
||||
import { addMarketplaceSource, saveMarketplaceToSettings } from '../../utils/plugins/marketplaceManager.js';
|
||||
import { parseMarketplaceInput } from '../../utils/plugins/parseMarketplaceInput.js';
|
||||
import type { ViewState } from './types.js';
|
||||
import * as React from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from 'src/services/analytics/index.js'
|
||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
|
||||
import { Byline } from '../../components/design-system/Byline.js'
|
||||
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js'
|
||||
import { Spinner } from '../../components/Spinner.js'
|
||||
import TextInput from '../../components/TextInput.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { toError } from '../../utils/errors.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import { clearAllCaches } from '../../utils/plugins/cacheUtils.js'
|
||||
import {
|
||||
addMarketplaceSource,
|
||||
saveMarketplaceToSettings,
|
||||
} from '../../utils/plugins/marketplaceManager.js'
|
||||
import { parseMarketplaceInput } from '../../utils/plugins/parseMarketplaceInput.js'
|
||||
import type { ViewState } from './types.js'
|
||||
|
||||
type Props = {
|
||||
inputValue: string;
|
||||
setInputValue: (value: string) => void;
|
||||
cursorOffset: number;
|
||||
setCursorOffset: (offset: number) => void;
|
||||
error: string | null;
|
||||
setError: (error: string | null) => void;
|
||||
result: string | null;
|
||||
setResult: (result: string | null) => void;
|
||||
setViewState: (state: ViewState) => void;
|
||||
onAddComplete?: () => void | Promise<void>;
|
||||
cliMode?: boolean;
|
||||
};
|
||||
inputValue: string
|
||||
setInputValue: (value: string) => void
|
||||
cursorOffset: number
|
||||
setCursorOffset: (offset: number) => void
|
||||
error: string | null
|
||||
setError: (error: string | null) => void
|
||||
result: string | null
|
||||
setResult: (result: string | null) => void
|
||||
setViewState: (state: ViewState) => void
|
||||
onAddComplete?: () => void | Promise<void>
|
||||
cliMode?: boolean
|
||||
}
|
||||
|
||||
export function AddMarketplace({
|
||||
inputValue,
|
||||
setInputValue,
|
||||
@@ -37,90 +45,100 @@ export function AddMarketplace({
|
||||
setResult,
|
||||
setViewState,
|
||||
onAddComplete,
|
||||
cliMode = false
|
||||
cliMode = false,
|
||||
}: Props): React.ReactNode {
|
||||
const hasAttemptedAutoAdd = useRef(false);
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const [progressMessage, setProgressMessage] = useState<string>('');
|
||||
const hasAttemptedAutoAdd = useRef(false)
|
||||
const [isLoading, setLoading] = useState(false)
|
||||
const [progressMessage, setProgressMessage] = useState<string>('')
|
||||
|
||||
const handleAdd = async () => {
|
||||
const input = inputValue.trim();
|
||||
const input = inputValue.trim()
|
||||
if (!input) {
|
||||
setError('Please enter a marketplace source');
|
||||
return;
|
||||
setError('Please enter a marketplace source')
|
||||
return
|
||||
}
|
||||
const parsed = await parseMarketplaceInput(input);
|
||||
|
||||
const parsed = await parseMarketplaceInput(input)
|
||||
if (!parsed) {
|
||||
setError('Invalid marketplace source format. Try: owner/repo, https://..., or ./path');
|
||||
return;
|
||||
setError(
|
||||
'Invalid marketplace source format. Try: owner/repo, https://..., or ./path',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if parseMarketplaceInput returned an error
|
||||
if ('error' in parsed) {
|
||||
setError(parsed.error);
|
||||
return;
|
||||
setError(parsed.error)
|
||||
return
|
||||
}
|
||||
setError(null);
|
||||
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setProgressMessage('');
|
||||
const {
|
||||
name,
|
||||
resolvedSource
|
||||
} = await addMarketplaceSource(parsed, message => {
|
||||
setProgressMessage(message);
|
||||
});
|
||||
saveMarketplaceToSettings(name, {
|
||||
source: resolvedSource
|
||||
});
|
||||
clearAllCaches();
|
||||
let sourceType = parsed.source;
|
||||
setLoading(true)
|
||||
setProgressMessage('')
|
||||
const { name, resolvedSource } = await addMarketplaceSource(
|
||||
parsed,
|
||||
message => {
|
||||
setProgressMessage(message)
|
||||
},
|
||||
)
|
||||
saveMarketplaceToSettings(name, { source: resolvedSource })
|
||||
clearAllCaches()
|
||||
|
||||
let sourceType = parsed.source
|
||||
if (parsed.source === 'github') {
|
||||
sourceType = parsed.repo as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS;
|
||||
sourceType =
|
||||
parsed.repo as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
}
|
||||
|
||||
logEvent('tengu_marketplace_added', {
|
||||
source_type: sourceType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
source_type:
|
||||
sourceType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
|
||||
if (onAddComplete) {
|
||||
await onAddComplete();
|
||||
await onAddComplete()
|
||||
}
|
||||
setProgressMessage('');
|
||||
setLoading(false);
|
||||
|
||||
setProgressMessage('')
|
||||
setLoading(false)
|
||||
|
||||
if (cliMode) {
|
||||
// In CLI mode, set result to trigger completion
|
||||
setResult(`Successfully added marketplace: ${name}`);
|
||||
setResult(`Successfully added marketplace: ${name}`)
|
||||
} else {
|
||||
// In interactive mode, switch to browse view
|
||||
setViewState({
|
||||
type: 'browse-marketplace',
|
||||
targetMarketplace: name
|
||||
});
|
||||
setViewState({ type: 'browse-marketplace', targetMarketplace: name })
|
||||
}
|
||||
} catch (err) {
|
||||
const error = toError(err);
|
||||
logError(error);
|
||||
setError(error.message);
|
||||
setProgressMessage('');
|
||||
setLoading(false);
|
||||
const error = toError(err)
|
||||
logError(error)
|
||||
setError(error.message)
|
||||
setProgressMessage('')
|
||||
setLoading(false)
|
||||
|
||||
if (cliMode) {
|
||||
// In CLI mode, set result with error to trigger completion
|
||||
setResult(`Error: ${error.message}`);
|
||||
setResult(`Error: ${error.message}`)
|
||||
} else {
|
||||
setResult(null);
|
||||
setResult(null)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Auto-add if inputValue is provided
|
||||
useEffect(() => {
|
||||
if (inputValue && !hasAttemptedAutoAdd.current && !error && !result) {
|
||||
hasAttemptedAutoAdd.current = true;
|
||||
void handleAdd();
|
||||
hasAttemptedAutoAdd.current = true
|
||||
void handleAdd()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional
|
||||
}, []); // Only run once on mount
|
||||
}, []) // Only run once on mount
|
||||
|
||||
return <Box flexDirection="column">
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box flexDirection="column" paddingX={1} borderStyle="round">
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>Add Marketplace</Text>
|
||||
@@ -133,29 +151,50 @@ export function AddMarketplace({
|
||||
<Text dimColor> · https://example.com/marketplace.json</Text>
|
||||
<Text dimColor> · ./path/to/marketplace</Text>
|
||||
<Box marginTop={1}>
|
||||
<TextInput value={inputValue} onChange={setInputValue} onSubmit={handleAdd} columns={80} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} focus showCursor />
|
||||
<TextInput
|
||||
value={inputValue}
|
||||
onChange={setInputValue}
|
||||
onSubmit={handleAdd}
|
||||
columns={80}
|
||||
cursorOffset={cursorOffset}
|
||||
onChangeCursorOffset={setCursorOffset}
|
||||
focus
|
||||
showCursor
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
{isLoading && <Box marginTop={1}>
|
||||
{isLoading && (
|
||||
<Box marginTop={1}>
|
||||
<Spinner />
|
||||
<Text>
|
||||
{progressMessage || 'Adding marketplace to configuration…'}
|
||||
</Text>
|
||||
</Box>}
|
||||
{error && <Box marginTop={1}>
|
||||
</Box>
|
||||
)}
|
||||
{error && (
|
||||
<Box marginTop={1}>
|
||||
<Text color="error">{error}</Text>
|
||||
</Box>}
|
||||
{result && <Box marginTop={1}>
|
||||
</Box>
|
||||
)}
|
||||
{result && (
|
||||
<Box marginTop={1}>
|
||||
<Text>{result}</Text>
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box marginLeft={3}>
|
||||
<Text dimColor italic>
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="Enter" action="add" />
|
||||
<ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="cancel" />
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Settings"
|
||||
fallback="Esc"
|
||||
description="cancel"
|
||||
/>
|
||||
</Byline>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,123 +1,140 @@
|
||||
import { getPluginErrorMessage, type PluginError } from '../../types/plugin.js';
|
||||
import { getPluginErrorMessage, type PluginError } from '../../types/plugin.js'
|
||||
|
||||
export function formatErrorMessage(error: PluginError): string {
|
||||
switch (error.type) {
|
||||
case 'path-not-found':
|
||||
return `${error.component} path not found: ${error.path}`;
|
||||
return `${error.component} path not found: ${error.path}`
|
||||
case 'git-auth-failed':
|
||||
return `Git ${error.authType.toUpperCase()} authentication failed for ${error.gitUrl}`;
|
||||
return `Git ${error.authType.toUpperCase()} authentication failed for ${error.gitUrl}`
|
||||
case 'git-timeout':
|
||||
return `Git ${error.operation} timed out for ${error.gitUrl}`;
|
||||
return `Git ${error.operation} timed out for ${error.gitUrl}`
|
||||
case 'network-error':
|
||||
return `Network error accessing ${error.url}${error.details ? `: ${error.details}` : ''}`;
|
||||
return `Network error accessing ${error.url}${error.details ? `: ${error.details}` : ''}`
|
||||
case 'manifest-parse-error':
|
||||
return `Failed to parse manifest at ${error.manifestPath}: ${error.parseError}`;
|
||||
return `Failed to parse manifest at ${error.manifestPath}: ${error.parseError}`
|
||||
case 'manifest-validation-error':
|
||||
return `Invalid manifest at ${error.manifestPath}: ${error.validationErrors.join(', ')}`;
|
||||
return `Invalid manifest at ${error.manifestPath}: ${error.validationErrors.join(', ')}`
|
||||
case 'plugin-not-found':
|
||||
return `Plugin "${error.pluginId}" not found in marketplace "${error.marketplace}"`;
|
||||
return `Plugin "${error.pluginId}" not found in marketplace "${error.marketplace}"`
|
||||
case 'marketplace-not-found':
|
||||
return `Marketplace "${error.marketplace}" not found`;
|
||||
return `Marketplace "${error.marketplace}" not found`
|
||||
case 'marketplace-load-failed':
|
||||
return `Failed to load marketplace "${error.marketplace}": ${error.reason}`;
|
||||
return `Failed to load marketplace "${error.marketplace}": ${error.reason}`
|
||||
case 'mcp-config-invalid':
|
||||
return `Invalid MCP server config for "${error.serverName}": ${error.validationError}`;
|
||||
case 'mcp-server-suppressed-duplicate':
|
||||
{
|
||||
const dup = error.duplicateOf.startsWith('plugin:') ? `server provided by plugin "${error.duplicateOf.split(':')[1] ?? '?'}"` : `already-configured "${error.duplicateOf}"`;
|
||||
return `MCP server "${error.serverName}" skipped — same command/URL as ${dup}`;
|
||||
}
|
||||
return `Invalid MCP server config for "${error.serverName}": ${error.validationError}`
|
||||
case 'mcp-server-suppressed-duplicate': {
|
||||
const dup = error.duplicateOf.startsWith('plugin:')
|
||||
? `server provided by plugin "${error.duplicateOf.split(':')[1] ?? '?'}"`
|
||||
: `already-configured "${error.duplicateOf}"`
|
||||
return `MCP server "${error.serverName}" skipped — same command/URL as ${dup}`
|
||||
}
|
||||
case 'hook-load-failed':
|
||||
return `Failed to load hooks from ${error.hookPath}: ${error.reason}`;
|
||||
return `Failed to load hooks from ${error.hookPath}: ${error.reason}`
|
||||
case 'component-load-failed':
|
||||
return `Failed to load ${error.component} from ${error.path}: ${error.reason}`;
|
||||
return `Failed to load ${error.component} from ${error.path}: ${error.reason}`
|
||||
case 'mcpb-download-failed':
|
||||
return `Failed to download MCPB from ${error.url}: ${error.reason}`;
|
||||
return `Failed to download MCPB from ${error.url}: ${error.reason}`
|
||||
case 'mcpb-extract-failed':
|
||||
return `Failed to extract MCPB ${error.mcpbPath}: ${error.reason}`;
|
||||
return `Failed to extract MCPB ${error.mcpbPath}: ${error.reason}`
|
||||
case 'mcpb-invalid-manifest':
|
||||
return `MCPB manifest invalid at ${error.mcpbPath}: ${error.validationError}`;
|
||||
return `MCPB manifest invalid at ${error.mcpbPath}: ${error.validationError}`
|
||||
case 'marketplace-blocked-by-policy':
|
||||
return error.blockedByBlocklist ? `Marketplace "${error.marketplace}" is blocked by enterprise policy` : `Marketplace "${error.marketplace}" is not in the allowed marketplace list`;
|
||||
return error.blockedByBlocklist
|
||||
? `Marketplace "${error.marketplace}" is blocked by enterprise policy`
|
||||
: `Marketplace "${error.marketplace}" is not in the allowed marketplace list`
|
||||
case 'dependency-unsatisfied':
|
||||
return error.reason === 'not-enabled' ? `Dependency "${error.dependency}" is disabled` : `Dependency "${error.dependency}" is not installed`;
|
||||
return error.reason === 'not-enabled'
|
||||
? `Dependency "${error.dependency}" is disabled`
|
||||
: `Dependency "${error.dependency}" is not installed`
|
||||
case 'lsp-config-invalid':
|
||||
return `Invalid LSP server config for "${error.serverName}": ${error.validationError}`;
|
||||
return `Invalid LSP server config for "${error.serverName}": ${error.validationError}`
|
||||
case 'lsp-server-start-failed':
|
||||
return `LSP server "${error.serverName}" failed to start: ${error.reason}`;
|
||||
return `LSP server "${error.serverName}" failed to start: ${error.reason}`
|
||||
case 'lsp-server-crashed':
|
||||
return error.signal ? `LSP server "${error.serverName}" crashed with signal ${error.signal}` : `LSP server "${error.serverName}" crashed with exit code ${error.exitCode ?? 'unknown'}`;
|
||||
return error.signal
|
||||
? `LSP server "${error.serverName}" crashed with signal ${error.signal}`
|
||||
: `LSP server "${error.serverName}" crashed with exit code ${error.exitCode ?? 'unknown'}`
|
||||
case 'lsp-request-timeout':
|
||||
return `LSP server "${error.serverName}" timed out on ${error.method} after ${error.timeoutMs}ms`;
|
||||
return `LSP server "${error.serverName}" timed out on ${error.method} after ${error.timeoutMs}ms`
|
||||
case 'lsp-request-failed':
|
||||
return `LSP server "${error.serverName}" ${error.method} failed: ${error.error}`;
|
||||
return `LSP server "${error.serverName}" ${error.method} failed: ${error.error}`
|
||||
case 'plugin-cache-miss':
|
||||
return `Plugin "${error.plugin}" not cached at ${error.installPath}`;
|
||||
return `Plugin "${error.plugin}" not cached at ${error.installPath}`
|
||||
case 'generic-error':
|
||||
return error.error;
|
||||
return error.error
|
||||
}
|
||||
const _exhaustive: never = error;
|
||||
return getPluginErrorMessage(_exhaustive);
|
||||
const _exhaustive: never = error
|
||||
return getPluginErrorMessage(_exhaustive)
|
||||
}
|
||||
|
||||
export function getErrorGuidance(error: PluginError): string | null {
|
||||
switch (error.type) {
|
||||
case 'path-not-found':
|
||||
return 'Check that the path in your manifest or marketplace config is correct';
|
||||
return 'Check that the path in your manifest or marketplace config is correct'
|
||||
case 'git-auth-failed':
|
||||
return error.authType === 'ssh' ? 'Configure SSH keys or use HTTPS URL instead' : 'Configure credentials or use SSH URL instead';
|
||||
return error.authType === 'ssh'
|
||||
? 'Configure SSH keys or use HTTPS URL instead'
|
||||
: 'Configure credentials or use SSH URL instead'
|
||||
case 'git-timeout':
|
||||
case 'network-error':
|
||||
return 'Check your internet connection and try again';
|
||||
return 'Check your internet connection and try again'
|
||||
case 'manifest-parse-error':
|
||||
return 'Check manifest file syntax in the plugin directory';
|
||||
return 'Check manifest file syntax in the plugin directory'
|
||||
case 'manifest-validation-error':
|
||||
return 'Check manifest file follows the required schema';
|
||||
return 'Check manifest file follows the required schema'
|
||||
case 'plugin-not-found':
|
||||
return `Plugin may not exist in marketplace "${error.marketplace}"`;
|
||||
return `Plugin may not exist in marketplace "${error.marketplace}"`
|
||||
case 'marketplace-not-found':
|
||||
return error.availableMarketplaces.length > 0 ? `Available marketplaces: ${error.availableMarketplaces.join(', ')}` : 'Add the marketplace first using /plugin marketplace add';
|
||||
return error.availableMarketplaces.length > 0
|
||||
? `Available marketplaces: ${error.availableMarketplaces.join(', ')}`
|
||||
: 'Add the marketplace first using /plugin marketplace add'
|
||||
case 'mcp-config-invalid':
|
||||
return 'Check MCP server configuration in .mcp.json or manifest';
|
||||
case 'mcp-server-suppressed-duplicate':
|
||||
{
|
||||
// duplicateOf is "plugin:name:srv" when another plugin won dedup —
|
||||
// users can't remove plugin-provided servers from their MCP config,
|
||||
// so point them at the winning plugin instead.
|
||||
if (error.duplicateOf.startsWith('plugin:')) {
|
||||
const winningPlugin = error.duplicateOf.split(':')[1] ?? 'the other plugin';
|
||||
return `Disable plugin "${winningPlugin}" if you want this plugin's version instead`;
|
||||
}
|
||||
return `Remove "${error.duplicateOf}" from your MCP config if you want the plugin's version instead`;
|
||||
return 'Check MCP server configuration in .mcp.json or manifest'
|
||||
case 'mcp-server-suppressed-duplicate': {
|
||||
// duplicateOf is "plugin:name:srv" when another plugin won dedup —
|
||||
// users can't remove plugin-provided servers from their MCP config,
|
||||
// so point them at the winning plugin instead.
|
||||
if (error.duplicateOf.startsWith('plugin:')) {
|
||||
const winningPlugin =
|
||||
error.duplicateOf.split(':')[1] ?? 'the other plugin'
|
||||
return `Disable plugin "${winningPlugin}" if you want this plugin's version instead`
|
||||
}
|
||||
return `Remove "${error.duplicateOf}" from your MCP config if you want the plugin's version instead`
|
||||
}
|
||||
case 'hook-load-failed':
|
||||
return 'Check hooks.json file syntax and structure';
|
||||
return 'Check hooks.json file syntax and structure'
|
||||
case 'component-load-failed':
|
||||
return `Check ${error.component} directory structure and file permissions`;
|
||||
return `Check ${error.component} directory structure and file permissions`
|
||||
case 'mcpb-download-failed':
|
||||
return 'Check your internet connection and URL accessibility';
|
||||
return 'Check your internet connection and URL accessibility'
|
||||
case 'mcpb-extract-failed':
|
||||
return 'Verify the MCPB file is valid and not corrupted';
|
||||
return 'Verify the MCPB file is valid and not corrupted'
|
||||
case 'mcpb-invalid-manifest':
|
||||
return 'Contact the plugin author about the invalid manifest';
|
||||
return 'Contact the plugin author about the invalid manifest'
|
||||
case 'marketplace-blocked-by-policy':
|
||||
if (error.blockedByBlocklist) {
|
||||
return 'This marketplace source is explicitly blocked by your administrator';
|
||||
return 'This marketplace source is explicitly blocked by your administrator'
|
||||
}
|
||||
return error.allowedSources.length > 0 ? `Allowed sources: ${error.allowedSources.join(', ')}` : 'Contact your administrator to configure allowed marketplace sources';
|
||||
return error.allowedSources.length > 0
|
||||
? `Allowed sources: ${error.allowedSources.join(', ')}`
|
||||
: 'Contact your administrator to configure allowed marketplace sources'
|
||||
case 'dependency-unsatisfied':
|
||||
return error.reason === 'not-enabled' ? `Enable "${error.dependency}" or uninstall "${error.plugin}"` : `Install "${error.dependency}" or uninstall "${error.plugin}"`;
|
||||
return error.reason === 'not-enabled'
|
||||
? `Enable "${error.dependency}" or uninstall "${error.plugin}"`
|
||||
: `Install "${error.dependency}" or uninstall "${error.plugin}"`
|
||||
case 'lsp-config-invalid':
|
||||
return 'Check LSP server configuration in the plugin manifest';
|
||||
return 'Check LSP server configuration in the plugin manifest'
|
||||
case 'lsp-server-start-failed':
|
||||
case 'lsp-server-crashed':
|
||||
case 'lsp-request-timeout':
|
||||
case 'lsp-request-failed':
|
||||
return 'Check LSP server logs with --debug for details';
|
||||
return 'Check LSP server logs with --debug for details'
|
||||
case 'plugin-cache-miss':
|
||||
return 'Run /plugins to refresh the plugin cache';
|
||||
return 'Run /plugins to refresh the plugin cache'
|
||||
case 'marketplace-load-failed':
|
||||
case 'generic-error':
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const _exhaustive: never = error;
|
||||
return null;
|
||||
const _exhaustive: never = error
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { stringWidth } from '../../ink/stringWidth.js';
|
||||
import figures from 'figures'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { stringWidth } from '../../ink/stringWidth.js'
|
||||
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw text input for config dialog
|
||||
import { Box, Text, useInput } from '../../ink.js';
|
||||
import { useKeybinding, useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js';
|
||||
import type { PluginOptionSchema, PluginOptionValues } from '../../utils/plugins/pluginOptionsStorage.js';
|
||||
import { Box, Text, useInput } from '../../ink.js'
|
||||
import {
|
||||
useKeybinding,
|
||||
useKeybindings,
|
||||
} from '../../keybindings/useKeybinding.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import type {
|
||||
PluginOptionSchema,
|
||||
PluginOptionValues,
|
||||
} from '../../utils/plugins/pluginOptionsStorage.js'
|
||||
|
||||
/**
|
||||
* Build the onSave payload from collected string inputs.
|
||||
@@ -21,336 +26,189 @@ import type { PluginOptionSchema, PluginOptionValues } from '../../utils/plugins
|
||||
*
|
||||
* Exported for unit testing.
|
||||
*/
|
||||
export function buildFinalValues(fields: string[], collected: Record<string, string>, configSchema: PluginOptionSchema, initialValues: PluginOptionValues | undefined): PluginOptionValues {
|
||||
const finalValues: PluginOptionValues = {};
|
||||
export function buildFinalValues(
|
||||
fields: string[],
|
||||
collected: Record<string, string>,
|
||||
configSchema: PluginOptionSchema,
|
||||
initialValues: PluginOptionValues | undefined,
|
||||
): PluginOptionValues {
|
||||
const finalValues: PluginOptionValues = {}
|
||||
for (const fieldKey of fields) {
|
||||
const schema = configSchema[fieldKey];
|
||||
const value = collected[fieldKey] ?? '';
|
||||
if (schema?.sensitive === true && value === '' && initialValues?.[fieldKey] !== undefined) {
|
||||
continue;
|
||||
const schema = configSchema[fieldKey]
|
||||
const value = collected[fieldKey] ?? ''
|
||||
|
||||
if (
|
||||
schema?.sensitive === true &&
|
||||
value === '' &&
|
||||
initialValues?.[fieldKey] !== undefined
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (schema?.type === 'number') {
|
||||
// Number('') returns 0, not NaN — omit blank number inputs so
|
||||
// validateUserConfig's required check actually catches them.
|
||||
if (value.trim() === '') continue;
|
||||
const num = Number(value);
|
||||
finalValues[fieldKey] = Number.isNaN(num) ? value : num;
|
||||
if (value.trim() === '') continue
|
||||
const num = Number(value)
|
||||
finalValues[fieldKey] = Number.isNaN(num) ? value : num
|
||||
} else if (schema?.type === 'boolean') {
|
||||
finalValues[fieldKey] = isEnvTruthy(value);
|
||||
finalValues[fieldKey] = isEnvTruthy(value)
|
||||
} else {
|
||||
finalValues[fieldKey] = value;
|
||||
finalValues[fieldKey] = value
|
||||
}
|
||||
}
|
||||
return finalValues;
|
||||
return finalValues
|
||||
}
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
configSchema: PluginOptionSchema;
|
||||
title: string
|
||||
subtitle: string
|
||||
configSchema: PluginOptionSchema
|
||||
/** Pre-fill fields when reconfiguring. Sensitive fields are not prepopulated. */
|
||||
initialValues?: PluginOptionValues;
|
||||
onSave: (config: PluginOptionValues) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
export function PluginOptionsDialog(t0) {
|
||||
const $ = _c(70);
|
||||
const {
|
||||
title,
|
||||
subtitle,
|
||||
initialValues?: PluginOptionValues
|
||||
onSave: (config: PluginOptionValues) => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export function PluginOptionsDialog({
|
||||
title,
|
||||
subtitle,
|
||||
configSchema,
|
||||
initialValues,
|
||||
onSave,
|
||||
onCancel,
|
||||
}: Props): React.ReactNode {
|
||||
const fields = Object.keys(configSchema)
|
||||
|
||||
// Prepopulate from initialValues but skip sensitive fields — we don't
|
||||
// want to echo secrets back into the text buffer.
|
||||
const initialFor = useCallback(
|
||||
(key: string): string => {
|
||||
if (configSchema[key]?.sensitive === true) return ''
|
||||
const v = initialValues?.[key]
|
||||
return v === undefined ? '' : String(v)
|
||||
},
|
||||
[configSchema, initialValues],
|
||||
)
|
||||
|
||||
const [currentFieldIndex, setCurrentFieldIndex] = useState(0)
|
||||
const [values, setValues] = useState<Record<string, string>>({})
|
||||
const [currentInput, setCurrentInput] = useState(() =>
|
||||
fields[0] ? initialFor(fields[0]) : '',
|
||||
)
|
||||
|
||||
const currentField = fields[currentFieldIndex]
|
||||
const fieldSchema = currentField ? configSchema[currentField] : null
|
||||
|
||||
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in input).
|
||||
// isCancelActive={false} on Dialog keeps its own confirm:no out of the way.
|
||||
useKeybinding('confirm:no', onCancel, { context: 'Settings' })
|
||||
|
||||
// Tab to next field
|
||||
const handleNextField = useCallback(() => {
|
||||
if (currentFieldIndex < fields.length - 1 && currentField) {
|
||||
setValues(prev => ({ ...prev, [currentField]: currentInput }))
|
||||
setCurrentFieldIndex(prev => prev + 1)
|
||||
const nextKey = fields[currentFieldIndex + 1]
|
||||
setCurrentInput(nextKey ? initialFor(nextKey) : '')
|
||||
}
|
||||
}, [currentFieldIndex, fields, currentField, currentInput, initialFor])
|
||||
|
||||
// Enter to save current field and move to next, or save all if last
|
||||
const handleConfirm = useCallback(() => {
|
||||
if (!currentField) return
|
||||
|
||||
const newValues = { ...values, [currentField]: currentInput }
|
||||
|
||||
if (currentFieldIndex === fields.length - 1) {
|
||||
onSave(buildFinalValues(fields, newValues, configSchema, initialValues))
|
||||
} else {
|
||||
// Move to next field
|
||||
setValues(newValues)
|
||||
setCurrentFieldIndex(prev => prev + 1)
|
||||
const nextKey = fields[currentFieldIndex + 1]
|
||||
setCurrentInput(nextKey ? initialFor(nextKey) : '')
|
||||
}
|
||||
}, [
|
||||
currentField,
|
||||
values,
|
||||
currentInput,
|
||||
currentFieldIndex,
|
||||
fields,
|
||||
configSchema,
|
||||
initialValues,
|
||||
onSave,
|
||||
onCancel
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== configSchema) {
|
||||
t1 = Object.keys(configSchema);
|
||||
$[0] = configSchema;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const fields = t1;
|
||||
let t2;
|
||||
if ($[2] !== configSchema || $[3] !== initialValues) {
|
||||
t2 = key => {
|
||||
if (configSchema[key]?.sensitive === true) {
|
||||
return "";
|
||||
}
|
||||
const v = initialValues?.[key];
|
||||
return v === undefined ? "" : String(v);
|
||||
};
|
||||
$[2] = configSchema;
|
||||
$[3] = initialValues;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
const initialFor = t2;
|
||||
const [currentFieldIndex, setCurrentFieldIndex] = useState(0);
|
||||
let t3;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = {};
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
const [values, setValues] = useState(t3);
|
||||
let t4;
|
||||
if ($[6] !== fields[0] || $[7] !== initialFor) {
|
||||
t4 = () => fields[0] ? initialFor(fields[0]) : "";
|
||||
$[6] = fields[0];
|
||||
$[7] = initialFor;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
const [currentInput, setCurrentInput] = useState(t4);
|
||||
const currentField = fields[currentFieldIndex];
|
||||
const fieldSchema = currentField ? configSchema[currentField] : null;
|
||||
let t5;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = {
|
||||
context: "Settings"
|
||||
};
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
useKeybinding("confirm:no", onCancel, t5);
|
||||
let t6;
|
||||
if ($[10] !== currentField || $[11] !== currentFieldIndex || $[12] !== currentInput || $[13] !== fields || $[14] !== initialFor) {
|
||||
t6 = () => {
|
||||
if (currentFieldIndex < fields.length - 1 && currentField) {
|
||||
setValues(prev => ({
|
||||
...prev,
|
||||
[currentField]: currentInput
|
||||
}));
|
||||
setCurrentFieldIndex(_temp);
|
||||
const nextKey = fields[currentFieldIndex + 1];
|
||||
setCurrentInput(nextKey ? initialFor(nextKey) : "");
|
||||
}
|
||||
};
|
||||
$[10] = currentField;
|
||||
$[11] = currentFieldIndex;
|
||||
$[12] = currentInput;
|
||||
$[13] = fields;
|
||||
$[14] = initialFor;
|
||||
$[15] = t6;
|
||||
} else {
|
||||
t6 = $[15];
|
||||
}
|
||||
const handleNextField = t6;
|
||||
let t7;
|
||||
if ($[16] !== configSchema || $[17] !== currentField || $[18] !== currentFieldIndex || $[19] !== currentInput || $[20] !== fields || $[21] !== initialFor || $[22] !== initialValues || $[23] !== onSave || $[24] !== values) {
|
||||
t7 = () => {
|
||||
if (!currentField) {
|
||||
return;
|
||||
}
|
||||
const newValues = {
|
||||
...values,
|
||||
[currentField]: currentInput
|
||||
};
|
||||
if (currentFieldIndex === fields.length - 1) {
|
||||
onSave(buildFinalValues(fields, newValues, configSchema, initialValues));
|
||||
} else {
|
||||
setValues(newValues);
|
||||
setCurrentFieldIndex(_temp2);
|
||||
const nextKey_0 = fields[currentFieldIndex + 1];
|
||||
setCurrentInput(nextKey_0 ? initialFor(nextKey_0) : "");
|
||||
}
|
||||
};
|
||||
$[16] = configSchema;
|
||||
$[17] = currentField;
|
||||
$[18] = currentFieldIndex;
|
||||
$[19] = currentInput;
|
||||
$[20] = fields;
|
||||
$[21] = initialFor;
|
||||
$[22] = initialValues;
|
||||
$[23] = onSave;
|
||||
$[24] = values;
|
||||
$[25] = t7;
|
||||
} else {
|
||||
t7 = $[25];
|
||||
}
|
||||
const handleConfirm = t7;
|
||||
let t8;
|
||||
if ($[26] !== handleConfirm || $[27] !== handleNextField) {
|
||||
t8 = {
|
||||
"confirm:nextField": handleNextField,
|
||||
"confirm:yes": handleConfirm
|
||||
};
|
||||
$[26] = handleConfirm;
|
||||
$[27] = handleNextField;
|
||||
$[28] = t8;
|
||||
} else {
|
||||
t8 = $[28];
|
||||
}
|
||||
let t9;
|
||||
if ($[29] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[29] = t9;
|
||||
} else {
|
||||
t9 = $[29];
|
||||
}
|
||||
useKeybindings(t8, t9);
|
||||
let t10;
|
||||
if ($[30] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t10 = (char, key_0) => {
|
||||
if (key_0.backspace || key_0.delete) {
|
||||
setCurrentInput(_temp3);
|
||||
return;
|
||||
}
|
||||
if (char && !key_0.ctrl && !key_0.meta && !key_0.tab && !key_0.return) {
|
||||
setCurrentInput(prev_3 => prev_3 + char);
|
||||
}
|
||||
};
|
||||
$[30] = t10;
|
||||
} else {
|
||||
t10 = $[30];
|
||||
}
|
||||
useInput(t10);
|
||||
initialFor,
|
||||
initialValues,
|
||||
])
|
||||
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:nextField': handleNextField,
|
||||
'confirm:yes': handleConfirm,
|
||||
},
|
||||
{ context: 'Confirmation' },
|
||||
)
|
||||
|
||||
// Character input handling (backspace, typing)
|
||||
useInput((char, key) => {
|
||||
// Backspace
|
||||
if (key.backspace || key.delete) {
|
||||
setCurrentInput(prev => prev.slice(0, -1))
|
||||
return
|
||||
}
|
||||
|
||||
// Regular character input
|
||||
if (char && !key.ctrl && !key.meta && !key.tab && !key.return) {
|
||||
setCurrentInput(prev => prev + char)
|
||||
}
|
||||
})
|
||||
|
||||
if (!fieldSchema || !currentField) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const isSensitive = fieldSchema.sensitive === true;
|
||||
const isRequired = fieldSchema.required === true;
|
||||
let t11;
|
||||
if ($[31] !== currentInput || $[32] !== isSensitive) {
|
||||
t11 = isSensitive ? "*".repeat(stringWidth(currentInput)) : currentInput;
|
||||
$[31] = currentInput;
|
||||
$[32] = isSensitive;
|
||||
$[33] = t11;
|
||||
} else {
|
||||
t11 = $[33];
|
||||
}
|
||||
const displayValue = t11;
|
||||
const t12 = fieldSchema.title || currentField;
|
||||
let t13;
|
||||
if ($[34] !== isRequired) {
|
||||
t13 = isRequired && <Text color="error"> *</Text>;
|
||||
$[34] = isRequired;
|
||||
$[35] = t13;
|
||||
} else {
|
||||
t13 = $[35];
|
||||
}
|
||||
let t14;
|
||||
if ($[36] !== t12 || $[37] !== t13) {
|
||||
t14 = <Text bold={true}>{t12}{t13}</Text>;
|
||||
$[36] = t12;
|
||||
$[37] = t13;
|
||||
$[38] = t14;
|
||||
} else {
|
||||
t14 = $[38];
|
||||
}
|
||||
let t15;
|
||||
if ($[39] !== fieldSchema.description) {
|
||||
t15 = fieldSchema.description && <Text dimColor={true}>{fieldSchema.description}</Text>;
|
||||
$[39] = fieldSchema.description;
|
||||
$[40] = t15;
|
||||
} else {
|
||||
t15 = $[40];
|
||||
}
|
||||
let t16;
|
||||
if ($[41] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t16 = <Text>{figures.pointerSmall} </Text>;
|
||||
$[41] = t16;
|
||||
} else {
|
||||
t16 = $[41];
|
||||
}
|
||||
let t17;
|
||||
if ($[42] !== displayValue) {
|
||||
t17 = <Text>{displayValue}</Text>;
|
||||
$[42] = displayValue;
|
||||
$[43] = t17;
|
||||
} else {
|
||||
t17 = $[43];
|
||||
}
|
||||
let t18;
|
||||
if ($[44] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t18 = <Text>█</Text>;
|
||||
$[44] = t18;
|
||||
} else {
|
||||
t18 = $[44];
|
||||
}
|
||||
let t19;
|
||||
if ($[45] !== t17) {
|
||||
t19 = <Box marginTop={1}>{t16}{t17}{t18}</Box>;
|
||||
$[45] = t17;
|
||||
$[46] = t19;
|
||||
} else {
|
||||
t19 = $[46];
|
||||
}
|
||||
let t20;
|
||||
if ($[47] !== t14 || $[48] !== t15 || $[49] !== t19) {
|
||||
t20 = <Box flexDirection="column">{t14}{t15}{t19}</Box>;
|
||||
$[47] = t14;
|
||||
$[48] = t15;
|
||||
$[49] = t19;
|
||||
$[50] = t20;
|
||||
} else {
|
||||
t20 = $[50];
|
||||
}
|
||||
const t21 = currentFieldIndex + 1;
|
||||
let t22;
|
||||
if ($[51] !== fields.length || $[52] !== t21) {
|
||||
t22 = <Text dimColor={true}>Field {t21} of {fields.length}</Text>;
|
||||
$[51] = fields.length;
|
||||
$[52] = t21;
|
||||
$[53] = t22;
|
||||
} else {
|
||||
t22 = $[53];
|
||||
}
|
||||
let t23;
|
||||
if ($[54] !== currentFieldIndex || $[55] !== fields.length) {
|
||||
t23 = currentFieldIndex < fields.length - 1 && <Text dimColor={true}>Tab: Next field · Enter: Save and continue</Text>;
|
||||
$[54] = currentFieldIndex;
|
||||
$[55] = fields.length;
|
||||
$[56] = t23;
|
||||
} else {
|
||||
t23 = $[56];
|
||||
}
|
||||
let t24;
|
||||
if ($[57] !== currentFieldIndex || $[58] !== fields.length) {
|
||||
t24 = currentFieldIndex === fields.length - 1 && <Text dimColor={true}>Enter: Save configuration</Text>;
|
||||
$[57] = currentFieldIndex;
|
||||
$[58] = fields.length;
|
||||
$[59] = t24;
|
||||
} else {
|
||||
t24 = $[59];
|
||||
}
|
||||
let t25;
|
||||
if ($[60] !== t22 || $[61] !== t23 || $[62] !== t24) {
|
||||
t25 = <Box flexDirection="column">{t22}{t23}{t24}</Box>;
|
||||
$[60] = t22;
|
||||
$[61] = t23;
|
||||
$[62] = t24;
|
||||
$[63] = t25;
|
||||
} else {
|
||||
t25 = $[63];
|
||||
}
|
||||
let t26;
|
||||
if ($[64] !== onCancel || $[65] !== subtitle || $[66] !== t20 || $[67] !== t25 || $[68] !== title) {
|
||||
t26 = <Dialog title={title} subtitle={subtitle} onCancel={onCancel} isCancelActive={false}>{t20}{t25}</Dialog>;
|
||||
$[64] = onCancel;
|
||||
$[65] = subtitle;
|
||||
$[66] = t20;
|
||||
$[67] = t25;
|
||||
$[68] = title;
|
||||
$[69] = t26;
|
||||
} else {
|
||||
t26 = $[69];
|
||||
}
|
||||
return t26;
|
||||
}
|
||||
function _temp3(prev_2) {
|
||||
return prev_2.slice(0, -1);
|
||||
}
|
||||
function _temp2(prev_1) {
|
||||
return prev_1 + 1;
|
||||
}
|
||||
function _temp(prev_0) {
|
||||
return prev_0 + 1;
|
||||
|
||||
const isSensitive = fieldSchema.sensitive === true
|
||||
const isRequired = fieldSchema.required === true
|
||||
const displayValue = isSensitive
|
||||
? '*'.repeat(stringWidth(currentInput))
|
||||
: currentInput
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
onCancel={onCancel}
|
||||
isCancelActive={false}
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
<Text bold={true}>
|
||||
{fieldSchema.title || currentField}
|
||||
{isRequired && <Text color="error"> *</Text>}
|
||||
</Text>
|
||||
{fieldSchema.description && (
|
||||
<Text dimColor={true}>{fieldSchema.description}</Text>
|
||||
)}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text>{figures.pointerSmall} </Text>
|
||||
<Text>{displayValue}</Text>
|
||||
<Text>█</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor={true}>
|
||||
Field {currentFieldIndex + 1} of {fields.length}
|
||||
</Text>
|
||||
{currentFieldIndex < fields.length - 1 && (
|
||||
<Text dimColor={true}>
|
||||
Tab: Next field · Enter: Save and continue
|
||||
</Text>
|
||||
)}
|
||||
{currentFieldIndex === fields.length - 1 && (
|
||||
<Text dimColor={true}>Enter: Save configuration</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,14 +7,26 @@
|
||||
* onDone('skipped') immediately if nothing needs filling.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import type { LoadedPlugin } from '../../types/plugin.js';
|
||||
import { errorMessage } from '../../utils/errors.js';
|
||||
import { loadMcpServerUserConfig, saveMcpServerUserConfig } from '../../utils/plugins/mcpbHandler.js';
|
||||
import { getUnconfiguredChannels, type UnconfiguredChannel } from '../../utils/plugins/mcpPluginIntegration.js';
|
||||
import { loadAllPlugins } from '../../utils/plugins/pluginLoader.js';
|
||||
import { getUnconfiguredOptions, loadPluginOptions, type PluginOptionSchema, type PluginOptionValues, savePluginOptions } from '../../utils/plugins/pluginOptionsStorage.js';
|
||||
import { PluginOptionsDialog } from './PluginOptionsDialog.js';
|
||||
import * as React from 'react'
|
||||
import type { LoadedPlugin } from '../../types/plugin.js'
|
||||
import { errorMessage } from '../../utils/errors.js'
|
||||
import {
|
||||
loadMcpServerUserConfig,
|
||||
saveMcpServerUserConfig,
|
||||
} from '../../utils/plugins/mcpbHandler.js'
|
||||
import {
|
||||
getUnconfiguredChannels,
|
||||
type UnconfiguredChannel,
|
||||
} from '../../utils/plugins/mcpPluginIntegration.js'
|
||||
import { loadAllPlugins } from '../../utils/plugins/pluginLoader.js'
|
||||
import {
|
||||
getUnconfiguredOptions,
|
||||
loadPluginOptions,
|
||||
type PluginOptionSchema,
|
||||
type PluginOptionValues,
|
||||
savePluginOptions,
|
||||
} from '../../utils/plugins/pluginOptionsStorage.js'
|
||||
import { PluginOptionsDialog } from './PluginOptionsDialog.js'
|
||||
|
||||
/**
|
||||
* Post-install lookup: return the LoadedPlugin for the just-installed
|
||||
@@ -24,12 +36,13 @@ import { PluginOptionsDialog } from './PluginOptionsDialog.js';
|
||||
*
|
||||
* Install should have cleared caches already; loadAllPlugins reads fresh.
|
||||
*/
|
||||
export async function findPluginOptionsTarget(pluginId: string): Promise<LoadedPlugin | undefined> {
|
||||
const {
|
||||
enabled,
|
||||
disabled
|
||||
} = await loadAllPlugins();
|
||||
return [...enabled, ...disabled].find(p => p.repository === pluginId || p.source === pluginId);
|
||||
export async function findPluginOptionsTarget(
|
||||
pluginId: string,
|
||||
): Promise<LoadedPlugin | undefined> {
|
||||
const { enabled, disabled } = await loadAllPlugins()
|
||||
return [...enabled, ...disabled].find(
|
||||
p => p.repository === pluginId || p.source === pluginId,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,37 +50,39 @@ export async function findPluginOptionsTarget(pluginId: string): Promise<LoadedP
|
||||
* collapse to this shape — the only difference is which save function runs.
|
||||
*/
|
||||
type ConfigStep = {
|
||||
key: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
schema: PluginOptionSchema;
|
||||
key: string
|
||||
title: string
|
||||
subtitle: string
|
||||
schema: PluginOptionSchema
|
||||
/** Returns any already-saved values so PluginOptionsDialog can pre-fill and
|
||||
* skip unchanged sensitive fields on reconfigure. */
|
||||
load: () => PluginOptionValues | undefined;
|
||||
save: (values: PluginOptionValues) => void;
|
||||
};
|
||||
load: () => PluginOptionValues | undefined
|
||||
save: (values: PluginOptionValues) => void
|
||||
}
|
||||
|
||||
type Props = {
|
||||
plugin: LoadedPlugin;
|
||||
plugin: LoadedPlugin
|
||||
/** `name@marketplace` — the savePluginOptions / saveMcpServerUserConfig key. */
|
||||
pluginId: string;
|
||||
pluginId: string
|
||||
/**
|
||||
* `configured` = user filled all fields. `skipped` = nothing needed
|
||||
* configuring, or user hit cancel. `error` = save threw.
|
||||
*/
|
||||
onDone: (outcome: 'configured' | 'skipped' | 'error', detail?: string) => void;
|
||||
};
|
||||
onDone: (outcome: 'configured' | 'skipped' | 'error', detail?: string) => void
|
||||
}
|
||||
|
||||
export function PluginOptionsFlow({
|
||||
plugin,
|
||||
pluginId,
|
||||
onDone
|
||||
onDone,
|
||||
}: Props): React.ReactNode {
|
||||
// Build the step list once at mount. Re-calling after a save would drop the
|
||||
// item we just configured.
|
||||
const [steps] = React.useState<ConfigStep[]>(() => {
|
||||
const result: ConfigStep[] = [];
|
||||
const result: ConfigStep[] = []
|
||||
|
||||
// Top-level manifest.userConfig
|
||||
const unconfigured = getUnconfiguredOptions(plugin);
|
||||
const unconfigured = getUnconfiguredOptions(plugin)
|
||||
if (Object.keys(unconfigured).length > 0) {
|
||||
result.push({
|
||||
key: 'top-level',
|
||||
@@ -75,60 +90,83 @@ export function PluginOptionsFlow({
|
||||
subtitle: 'Plugin options',
|
||||
schema: unconfigured,
|
||||
load: () => loadPluginOptions(pluginId),
|
||||
save: values => savePluginOptions(pluginId, values, plugin.manifest.userConfig!)
|
||||
});
|
||||
save: values =>
|
||||
savePluginOptions(pluginId, values, plugin.manifest.userConfig!),
|
||||
})
|
||||
}
|
||||
|
||||
// Per-channel userConfig (assistant-mode channels)
|
||||
const channels: UnconfiguredChannel[] = getUnconfiguredChannels(plugin);
|
||||
const channels: UnconfiguredChannel[] = getUnconfiguredChannels(plugin)
|
||||
for (const channel of channels) {
|
||||
result.push({
|
||||
key: `channel:${channel.server}`,
|
||||
title: `Configure ${channel.displayName}`,
|
||||
subtitle: `Plugin: ${plugin.name}`,
|
||||
schema: channel.configSchema,
|
||||
load: () => loadMcpServerUserConfig(pluginId, channel.server) ?? undefined,
|
||||
save: values_0 => saveMcpServerUserConfig(pluginId, channel.server, values_0, channel.configSchema)
|
||||
});
|
||||
load: () =>
|
||||
loadMcpServerUserConfig(pluginId, channel.server) ?? undefined,
|
||||
save: values =>
|
||||
saveMcpServerUserConfig(
|
||||
pluginId,
|
||||
channel.server,
|
||||
values,
|
||||
channel.configSchema,
|
||||
),
|
||||
})
|
||||
}
|
||||
return result;
|
||||
});
|
||||
const [index, setIndex] = React.useState(0);
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const [index, setIndex] = React.useState(0)
|
||||
|
||||
// Latest-ref: lets the effect close over the current onDone without
|
||||
// re-running when the parent re-renders.
|
||||
const onDoneRef = React.useRef(onDone);
|
||||
onDoneRef.current = onDone;
|
||||
const onDoneRef = React.useRef(onDone)
|
||||
onDoneRef.current = onDone
|
||||
|
||||
// Nothing to configure → tell the caller and render nothing. Effect,
|
||||
// not inline call: calling setState in the parent during our render
|
||||
// is a React rules-of-hooks violation.
|
||||
React.useEffect(() => {
|
||||
if (steps.length === 0) {
|
||||
onDoneRef.current('skipped');
|
||||
onDoneRef.current('skipped')
|
||||
}
|
||||
}, [steps.length]);
|
||||
}, [steps.length])
|
||||
|
||||
if (steps.length === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const current = steps[index]!;
|
||||
function handleSave(values_1: PluginOptionValues): void {
|
||||
|
||||
const current = steps[index]!
|
||||
|
||||
function handleSave(values: PluginOptionValues): void {
|
||||
try {
|
||||
current.save(values_1);
|
||||
current.save(values)
|
||||
} catch (err) {
|
||||
onDone('error', errorMessage(err));
|
||||
return;
|
||||
onDone('error', errorMessage(err))
|
||||
return
|
||||
}
|
||||
const next = index + 1;
|
||||
const next = index + 1
|
||||
if (next < steps.length) {
|
||||
setIndex(next);
|
||||
setIndex(next)
|
||||
} else {
|
||||
onDone('configured');
|
||||
onDone('configured')
|
||||
}
|
||||
}
|
||||
|
||||
// key forces a remount when advancing to the next step — React would
|
||||
// otherwise reuse the instance and carry PluginOptionsDialog's
|
||||
// internal useState (field index, typed values) over.
|
||||
return <PluginOptionsDialog key={current.key} title={current.title} subtitle={current.subtitle} configSchema={current.schema} initialValues={current.load()} onSave={handleSave} onCancel={() => onDone('skipped')} />;
|
||||
return (
|
||||
<PluginOptionsDialog
|
||||
key={current.key}
|
||||
title={current.title}
|
||||
subtitle={current.subtitle}
|
||||
configSchema={current.schema}
|
||||
initialValues={current.load()}
|
||||
onSave={handleSave}
|
||||
onCancel={() => onDone('skipped')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,31 +1,20 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { getPluginTrustMessage } from '../../utils/plugins/marketplaceHelpers.js';
|
||||
export function PluginTrustWarning() {
|
||||
const $ = _c(3);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = getPluginTrustMessage();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const customMessage = t0;
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text color="claude">{figures.warning} </Text>;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Box marginBottom={1}>{t1}<Text dimColor={true} italic={true}>Make sure you trust a plugin before installing, updating, or using it. Anthropic does not control what MCP servers, files, or other software are included in plugins and cannot verify that they will work as intended or that they won't change. See each plugin's homepage for more information.{customMessage ? ` ${customMessage}` : ""}</Text></Box>;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
return t2;
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { getPluginTrustMessage } from '../../utils/plugins/marketplaceHelpers.js'
|
||||
|
||||
export function PluginTrustWarning(): React.ReactNode {
|
||||
const customMessage = getPluginTrustMessage()
|
||||
return (
|
||||
<Box marginBottom={1}>
|
||||
<Text color="claude">{figures.warning} </Text>
|
||||
<Text dimColor italic>
|
||||
Make sure you trust a plugin before installing, updating, or using it.
|
||||
Anthropic does not control what MCP servers, files, or other software
|
||||
are included in plugins and cannot verify that they will work as
|
||||
intended or that they won't change. See each plugin's homepage
|
||||
for more information.{customMessage ? ` ${customMessage}` : ''}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,564 +1,151 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { Box, color, Text, useTheme } from '../../ink.js';
|
||||
import { plural } from '../../utils/stringUtils.js';
|
||||
import type { UnifiedInstalledItem } from './unifiedTypes.js';
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { Box, color, Text, useTheme } from '../../ink.js'
|
||||
import { plural } from '../../utils/stringUtils.js'
|
||||
import type { UnifiedInstalledItem } from './unifiedTypes.js'
|
||||
|
||||
type Props = {
|
||||
item: UnifiedInstalledItem;
|
||||
isSelected: boolean;
|
||||
};
|
||||
export function UnifiedInstalledCell(t0) {
|
||||
const $ = _c(142);
|
||||
const {
|
||||
item,
|
||||
isSelected
|
||||
} = t0;
|
||||
const [theme] = useTheme();
|
||||
if (item.type === "plugin") {
|
||||
let statusIcon;
|
||||
let statusText;
|
||||
if (item.pendingToggle) {
|
||||
let t1;
|
||||
if ($[0] !== theme) {
|
||||
t1 = color("suggestion", theme)(figures.arrowRight);
|
||||
$[0] = theme;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
statusIcon = t1;
|
||||
statusText = item.pendingToggle === "will-enable" ? "will enable" : "will disable";
|
||||
} else {
|
||||
if (item.errorCount > 0) {
|
||||
let t1;
|
||||
if ($[2] !== theme) {
|
||||
t1 = color("error", theme)(figures.cross);
|
||||
$[2] = theme;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
statusIcon = t1;
|
||||
const t2 = item.errorCount;
|
||||
let t3;
|
||||
if ($[4] !== item.errorCount) {
|
||||
t3 = plural(item.errorCount, "error");
|
||||
$[4] = item.errorCount;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
statusText = `${t2} ${t3}`;
|
||||
} else {
|
||||
if (!item.isEnabled) {
|
||||
let t1;
|
||||
if ($[6] !== theme) {
|
||||
t1 = color("inactive", theme)(figures.radioOff);
|
||||
$[6] = theme;
|
||||
$[7] = t1;
|
||||
} else {
|
||||
t1 = $[7];
|
||||
}
|
||||
statusIcon = t1;
|
||||
statusText = "disabled";
|
||||
} else {
|
||||
let t1;
|
||||
if ($[8] !== theme) {
|
||||
t1 = color("success", theme)(figures.tick);
|
||||
$[8] = theme;
|
||||
$[9] = t1;
|
||||
} else {
|
||||
t1 = $[9];
|
||||
}
|
||||
statusIcon = t1;
|
||||
statusText = "enabled";
|
||||
}
|
||||
}
|
||||
}
|
||||
const t1 = isSelected ? "suggestion" : undefined;
|
||||
const t2 = isSelected ? `${figures.pointer} ` : " ";
|
||||
let t3;
|
||||
if ($[10] !== t1 || $[11] !== t2) {
|
||||
t3 = <Text color={t1}>{t2}</Text>;
|
||||
$[10] = t1;
|
||||
$[11] = t2;
|
||||
$[12] = t3;
|
||||
} else {
|
||||
t3 = $[12];
|
||||
}
|
||||
const t4 = isSelected ? "suggestion" : undefined;
|
||||
let t5;
|
||||
if ($[13] !== item.name || $[14] !== t4) {
|
||||
t5 = <Text color={t4}>{item.name}</Text>;
|
||||
$[13] = item.name;
|
||||
$[14] = t4;
|
||||
$[15] = t5;
|
||||
} else {
|
||||
t5 = $[15];
|
||||
}
|
||||
const t6 = !isSelected;
|
||||
let t7;
|
||||
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Text backgroundColor="userMessageBackground">Plugin</Text>;
|
||||
$[16] = t7;
|
||||
} else {
|
||||
t7 = $[16];
|
||||
}
|
||||
let t8;
|
||||
if ($[17] !== t6) {
|
||||
t8 = <Text dimColor={t6}>{" "}{t7}</Text>;
|
||||
$[17] = t6;
|
||||
$[18] = t8;
|
||||
} else {
|
||||
t8 = $[18];
|
||||
}
|
||||
let t9;
|
||||
if ($[19] !== item.marketplace) {
|
||||
t9 = <Text dimColor={true}> · {item.marketplace}</Text>;
|
||||
$[19] = item.marketplace;
|
||||
$[20] = t9;
|
||||
} else {
|
||||
t9 = $[20];
|
||||
}
|
||||
const t10 = !isSelected;
|
||||
let t11;
|
||||
if ($[21] !== statusIcon || $[22] !== t10) {
|
||||
t11 = <Text dimColor={t10}> · {statusIcon} </Text>;
|
||||
$[21] = statusIcon;
|
||||
$[22] = t10;
|
||||
$[23] = t11;
|
||||
} else {
|
||||
t11 = $[23];
|
||||
}
|
||||
const t12 = !isSelected;
|
||||
let t13;
|
||||
if ($[24] !== statusText || $[25] !== t12) {
|
||||
t13 = <Text dimColor={t12}>{statusText}</Text>;
|
||||
$[24] = statusText;
|
||||
$[25] = t12;
|
||||
$[26] = t13;
|
||||
} else {
|
||||
t13 = $[26];
|
||||
}
|
||||
let t14;
|
||||
if ($[27] !== t11 || $[28] !== t13 || $[29] !== t3 || $[30] !== t5 || $[31] !== t8 || $[32] !== t9) {
|
||||
t14 = <Box>{t3}{t5}{t8}{t9}{t11}{t13}</Box>;
|
||||
$[27] = t11;
|
||||
$[28] = t13;
|
||||
$[29] = t3;
|
||||
$[30] = t5;
|
||||
$[31] = t8;
|
||||
$[32] = t9;
|
||||
$[33] = t14;
|
||||
} else {
|
||||
t14 = $[33];
|
||||
}
|
||||
return t14;
|
||||
}
|
||||
if (item.type === "flagged-plugin") {
|
||||
let t1;
|
||||
if ($[34] !== theme) {
|
||||
t1 = color("warning", theme)(figures.warning);
|
||||
$[34] = theme;
|
||||
$[35] = t1;
|
||||
} else {
|
||||
t1 = $[35];
|
||||
}
|
||||
const statusIcon_0 = t1;
|
||||
const t2 = isSelected ? "suggestion" : undefined;
|
||||
const t3 = isSelected ? `${figures.pointer} ` : " ";
|
||||
let t4;
|
||||
if ($[36] !== t2 || $[37] !== t3) {
|
||||
t4 = <Text color={t2}>{t3}</Text>;
|
||||
$[36] = t2;
|
||||
$[37] = t3;
|
||||
$[38] = t4;
|
||||
} else {
|
||||
t4 = $[38];
|
||||
}
|
||||
const t5 = isSelected ? "suggestion" : undefined;
|
||||
let t6;
|
||||
if ($[39] !== item.name || $[40] !== t5) {
|
||||
t6 = <Text color={t5}>{item.name}</Text>;
|
||||
$[39] = item.name;
|
||||
$[40] = t5;
|
||||
$[41] = t6;
|
||||
} else {
|
||||
t6 = $[41];
|
||||
}
|
||||
const t7 = !isSelected;
|
||||
let t8;
|
||||
if ($[42] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = <Text backgroundColor="userMessageBackground">Plugin</Text>;
|
||||
$[42] = t8;
|
||||
} else {
|
||||
t8 = $[42];
|
||||
}
|
||||
let t9;
|
||||
if ($[43] !== t7) {
|
||||
t9 = <Text dimColor={t7}>{" "}{t8}</Text>;
|
||||
$[43] = t7;
|
||||
$[44] = t9;
|
||||
} else {
|
||||
t9 = $[44];
|
||||
}
|
||||
let t10;
|
||||
if ($[45] !== item.marketplace) {
|
||||
t10 = <Text dimColor={true}> · {item.marketplace}</Text>;
|
||||
$[45] = item.marketplace;
|
||||
$[46] = t10;
|
||||
} else {
|
||||
t10 = $[46];
|
||||
}
|
||||
const t11 = !isSelected;
|
||||
let t12;
|
||||
if ($[47] !== statusIcon_0 || $[48] !== t11) {
|
||||
t12 = <Text dimColor={t11}> · {statusIcon_0} </Text>;
|
||||
$[47] = statusIcon_0;
|
||||
$[48] = t11;
|
||||
$[49] = t12;
|
||||
} else {
|
||||
t12 = $[49];
|
||||
}
|
||||
const t13 = !isSelected;
|
||||
let t14;
|
||||
if ($[50] !== t13) {
|
||||
t14 = <Text dimColor={t13}>removed</Text>;
|
||||
$[50] = t13;
|
||||
$[51] = t14;
|
||||
} else {
|
||||
t14 = $[51];
|
||||
}
|
||||
let t15;
|
||||
if ($[52] !== t10 || $[53] !== t12 || $[54] !== t14 || $[55] !== t4 || $[56] !== t6 || $[57] !== t9) {
|
||||
t15 = <Box>{t4}{t6}{t9}{t10}{t12}{t14}</Box>;
|
||||
$[52] = t10;
|
||||
$[53] = t12;
|
||||
$[54] = t14;
|
||||
$[55] = t4;
|
||||
$[56] = t6;
|
||||
$[57] = t9;
|
||||
$[58] = t15;
|
||||
} else {
|
||||
t15 = $[58];
|
||||
}
|
||||
return t15;
|
||||
}
|
||||
if (item.type === "failed-plugin") {
|
||||
let t1;
|
||||
if ($[59] !== theme) {
|
||||
t1 = color("error", theme)(figures.cross);
|
||||
$[59] = theme;
|
||||
$[60] = t1;
|
||||
} else {
|
||||
t1 = $[60];
|
||||
}
|
||||
const statusIcon_1 = t1;
|
||||
const t2 = item.errorCount;
|
||||
let t3;
|
||||
if ($[61] !== item.errorCount) {
|
||||
t3 = plural(item.errorCount, "error");
|
||||
$[61] = item.errorCount;
|
||||
$[62] = t3;
|
||||
} else {
|
||||
t3 = $[62];
|
||||
}
|
||||
const statusText_0 = `failed to load · ${t2} ${t3}`;
|
||||
const t4 = isSelected ? "suggestion" : undefined;
|
||||
const t5 = isSelected ? `${figures.pointer} ` : " ";
|
||||
let t6;
|
||||
if ($[63] !== t4 || $[64] !== t5) {
|
||||
t6 = <Text color={t4}>{t5}</Text>;
|
||||
$[63] = t4;
|
||||
$[64] = t5;
|
||||
$[65] = t6;
|
||||
} else {
|
||||
t6 = $[65];
|
||||
}
|
||||
const t7 = isSelected ? "suggestion" : undefined;
|
||||
let t8;
|
||||
if ($[66] !== item.name || $[67] !== t7) {
|
||||
t8 = <Text color={t7}>{item.name}</Text>;
|
||||
$[66] = item.name;
|
||||
$[67] = t7;
|
||||
$[68] = t8;
|
||||
} else {
|
||||
t8 = $[68];
|
||||
}
|
||||
const t9 = !isSelected;
|
||||
let t10;
|
||||
if ($[69] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t10 = <Text backgroundColor="userMessageBackground">Plugin</Text>;
|
||||
$[69] = t10;
|
||||
} else {
|
||||
t10 = $[69];
|
||||
}
|
||||
let t11;
|
||||
if ($[70] !== t9) {
|
||||
t11 = <Text dimColor={t9}>{" "}{t10}</Text>;
|
||||
$[70] = t9;
|
||||
$[71] = t11;
|
||||
} else {
|
||||
t11 = $[71];
|
||||
}
|
||||
let t12;
|
||||
if ($[72] !== item.marketplace) {
|
||||
t12 = <Text dimColor={true}> · {item.marketplace}</Text>;
|
||||
$[72] = item.marketplace;
|
||||
$[73] = t12;
|
||||
} else {
|
||||
t12 = $[73];
|
||||
}
|
||||
const t13 = !isSelected;
|
||||
let t14;
|
||||
if ($[74] !== statusIcon_1 || $[75] !== t13) {
|
||||
t14 = <Text dimColor={t13}> · {statusIcon_1} </Text>;
|
||||
$[74] = statusIcon_1;
|
||||
$[75] = t13;
|
||||
$[76] = t14;
|
||||
} else {
|
||||
t14 = $[76];
|
||||
}
|
||||
const t15 = !isSelected;
|
||||
let t16;
|
||||
if ($[77] !== statusText_0 || $[78] !== t15) {
|
||||
t16 = <Text dimColor={t15}>{statusText_0}</Text>;
|
||||
$[77] = statusText_0;
|
||||
$[78] = t15;
|
||||
$[79] = t16;
|
||||
} else {
|
||||
t16 = $[79];
|
||||
}
|
||||
let t17;
|
||||
if ($[80] !== t11 || $[81] !== t12 || $[82] !== t14 || $[83] !== t16 || $[84] !== t6 || $[85] !== t8) {
|
||||
t17 = <Box>{t6}{t8}{t11}{t12}{t14}{t16}</Box>;
|
||||
$[80] = t11;
|
||||
$[81] = t12;
|
||||
$[82] = t14;
|
||||
$[83] = t16;
|
||||
$[84] = t6;
|
||||
$[85] = t8;
|
||||
$[86] = t17;
|
||||
} else {
|
||||
t17 = $[86];
|
||||
}
|
||||
return t17;
|
||||
}
|
||||
let statusIcon_2;
|
||||
let statusText_1;
|
||||
if (item.status === "connected") {
|
||||
let t1;
|
||||
if ($[87] !== theme) {
|
||||
t1 = color("success", theme)(figures.tick);
|
||||
$[87] = theme;
|
||||
$[88] = t1;
|
||||
} else {
|
||||
t1 = $[88];
|
||||
}
|
||||
statusIcon_2 = t1;
|
||||
statusText_1 = "connected";
|
||||
} else {
|
||||
if (item.status === "disabled") {
|
||||
let t1;
|
||||
if ($[89] !== theme) {
|
||||
t1 = color("inactive", theme)(figures.radioOff);
|
||||
$[89] = theme;
|
||||
$[90] = t1;
|
||||
} else {
|
||||
t1 = $[90];
|
||||
}
|
||||
statusIcon_2 = t1;
|
||||
statusText_1 = "disabled";
|
||||
} else {
|
||||
if (item.status === "pending") {
|
||||
let t1;
|
||||
if ($[91] !== theme) {
|
||||
t1 = color("inactive", theme)(figures.radioOff);
|
||||
$[91] = theme;
|
||||
$[92] = t1;
|
||||
} else {
|
||||
t1 = $[92];
|
||||
}
|
||||
statusIcon_2 = t1;
|
||||
statusText_1 = "connecting\u2026";
|
||||
} else {
|
||||
if (item.status === "needs-auth") {
|
||||
let t1;
|
||||
if ($[93] !== theme) {
|
||||
t1 = color("warning", theme)(figures.triangleUpOutline);
|
||||
$[93] = theme;
|
||||
$[94] = t1;
|
||||
} else {
|
||||
t1 = $[94];
|
||||
}
|
||||
statusIcon_2 = t1;
|
||||
statusText_1 = "Enter to auth";
|
||||
} else {
|
||||
let t1;
|
||||
if ($[95] !== theme) {
|
||||
t1 = color("error", theme)(figures.cross);
|
||||
$[95] = theme;
|
||||
$[96] = t1;
|
||||
} else {
|
||||
t1 = $[96];
|
||||
}
|
||||
statusIcon_2 = t1;
|
||||
statusText_1 = "failed";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (item.indented) {
|
||||
const t1 = isSelected ? "suggestion" : undefined;
|
||||
const t2 = isSelected ? `${figures.pointer} ` : " ";
|
||||
let t3;
|
||||
if ($[97] !== t1 || $[98] !== t2) {
|
||||
t3 = <Text color={t1}>{t2}</Text>;
|
||||
$[97] = t1;
|
||||
$[98] = t2;
|
||||
$[99] = t3;
|
||||
} else {
|
||||
t3 = $[99];
|
||||
}
|
||||
const t4 = !isSelected;
|
||||
let t5;
|
||||
if ($[100] !== t4) {
|
||||
t5 = <Text dimColor={t4}>└ </Text>;
|
||||
$[100] = t4;
|
||||
$[101] = t5;
|
||||
} else {
|
||||
t5 = $[101];
|
||||
}
|
||||
const t6 = isSelected ? "suggestion" : undefined;
|
||||
let t7;
|
||||
if ($[102] !== item.name || $[103] !== t6) {
|
||||
t7 = <Text color={t6}>{item.name}</Text>;
|
||||
$[102] = item.name;
|
||||
$[103] = t6;
|
||||
$[104] = t7;
|
||||
} else {
|
||||
t7 = $[104];
|
||||
}
|
||||
const t8 = !isSelected;
|
||||
let t9;
|
||||
if ($[105] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = <Text backgroundColor="userMessageBackground">MCP</Text>;
|
||||
$[105] = t9;
|
||||
} else {
|
||||
t9 = $[105];
|
||||
}
|
||||
let t10;
|
||||
if ($[106] !== t8) {
|
||||
t10 = <Text dimColor={t8}>{" "}{t9}</Text>;
|
||||
$[106] = t8;
|
||||
$[107] = t10;
|
||||
} else {
|
||||
t10 = $[107];
|
||||
}
|
||||
const t11 = !isSelected;
|
||||
let t12;
|
||||
if ($[108] !== statusIcon_2 || $[109] !== t11) {
|
||||
t12 = <Text dimColor={t11}> · {statusIcon_2} </Text>;
|
||||
$[108] = statusIcon_2;
|
||||
$[109] = t11;
|
||||
$[110] = t12;
|
||||
} else {
|
||||
t12 = $[110];
|
||||
}
|
||||
const t13 = !isSelected;
|
||||
let t14;
|
||||
if ($[111] !== statusText_1 || $[112] !== t13) {
|
||||
t14 = <Text dimColor={t13}>{statusText_1}</Text>;
|
||||
$[111] = statusText_1;
|
||||
$[112] = t13;
|
||||
$[113] = t14;
|
||||
} else {
|
||||
t14 = $[113];
|
||||
}
|
||||
let t15;
|
||||
if ($[114] !== t10 || $[115] !== t12 || $[116] !== t14 || $[117] !== t3 || $[118] !== t5 || $[119] !== t7) {
|
||||
t15 = <Box>{t3}{t5}{t7}{t10}{t12}{t14}</Box>;
|
||||
$[114] = t10;
|
||||
$[115] = t12;
|
||||
$[116] = t14;
|
||||
$[117] = t3;
|
||||
$[118] = t5;
|
||||
$[119] = t7;
|
||||
$[120] = t15;
|
||||
} else {
|
||||
t15 = $[120];
|
||||
}
|
||||
return t15;
|
||||
}
|
||||
const t1 = isSelected ? "suggestion" : undefined;
|
||||
const t2 = isSelected ? `${figures.pointer} ` : " ";
|
||||
let t3;
|
||||
if ($[121] !== t1 || $[122] !== t2) {
|
||||
t3 = <Text color={t1}>{t2}</Text>;
|
||||
$[121] = t1;
|
||||
$[122] = t2;
|
||||
$[123] = t3;
|
||||
} else {
|
||||
t3 = $[123];
|
||||
}
|
||||
const t4 = isSelected ? "suggestion" : undefined;
|
||||
let t5;
|
||||
if ($[124] !== item.name || $[125] !== t4) {
|
||||
t5 = <Text color={t4}>{item.name}</Text>;
|
||||
$[124] = item.name;
|
||||
$[125] = t4;
|
||||
$[126] = t5;
|
||||
} else {
|
||||
t5 = $[126];
|
||||
}
|
||||
const t6 = !isSelected;
|
||||
let t7;
|
||||
if ($[127] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Text backgroundColor="userMessageBackground">MCP</Text>;
|
||||
$[127] = t7;
|
||||
} else {
|
||||
t7 = $[127];
|
||||
}
|
||||
let t8;
|
||||
if ($[128] !== t6) {
|
||||
t8 = <Text dimColor={t6}>{" "}{t7}</Text>;
|
||||
$[128] = t6;
|
||||
$[129] = t8;
|
||||
} else {
|
||||
t8 = $[129];
|
||||
}
|
||||
const t9 = !isSelected;
|
||||
let t10;
|
||||
if ($[130] !== statusIcon_2 || $[131] !== t9) {
|
||||
t10 = <Text dimColor={t9}> · {statusIcon_2} </Text>;
|
||||
$[130] = statusIcon_2;
|
||||
$[131] = t9;
|
||||
$[132] = t10;
|
||||
} else {
|
||||
t10 = $[132];
|
||||
}
|
||||
const t11 = !isSelected;
|
||||
let t12;
|
||||
if ($[133] !== statusText_1 || $[134] !== t11) {
|
||||
t12 = <Text dimColor={t11}>{statusText_1}</Text>;
|
||||
$[133] = statusText_1;
|
||||
$[134] = t11;
|
||||
$[135] = t12;
|
||||
} else {
|
||||
t12 = $[135];
|
||||
}
|
||||
let t13;
|
||||
if ($[136] !== t10 || $[137] !== t12 || $[138] !== t3 || $[139] !== t5 || $[140] !== t8) {
|
||||
t13 = <Box>{t3}{t5}{t8}{t10}{t12}</Box>;
|
||||
$[136] = t10;
|
||||
$[137] = t12;
|
||||
$[138] = t3;
|
||||
$[139] = t5;
|
||||
$[140] = t8;
|
||||
$[141] = t13;
|
||||
} else {
|
||||
t13 = $[141];
|
||||
}
|
||||
return t13;
|
||||
item: UnifiedInstalledItem
|
||||
isSelected: boolean
|
||||
}
|
||||
|
||||
export function UnifiedInstalledCell({
|
||||
item,
|
||||
isSelected,
|
||||
}: Props): React.ReactNode {
|
||||
const [theme] = useTheme()
|
||||
|
||||
if (item.type === 'plugin') {
|
||||
// Status icon and text
|
||||
let statusIcon: string
|
||||
let statusText: string
|
||||
|
||||
// Show pending toggle status if set, otherwise show current status
|
||||
if (item.pendingToggle) {
|
||||
statusIcon = color('suggestion', theme)(figures.arrowRight)
|
||||
statusText =
|
||||
item.pendingToggle === 'will-enable' ? 'will enable' : 'will disable'
|
||||
} else if (item.errorCount > 0) {
|
||||
statusIcon = color('error', theme)(figures.cross)
|
||||
statusText = `${item.errorCount} ${plural(item.errorCount, 'error')}`
|
||||
} else if (!item.isEnabled) {
|
||||
statusIcon = color('inactive', theme)(figures.radioOff)
|
||||
statusText = 'disabled'
|
||||
} else {
|
||||
statusIcon = color('success', theme)(figures.tick)
|
||||
statusText = 'enabled'
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
||||
{isSelected ? `${figures.pointer} ` : ' '}
|
||||
</Text>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>{item.name}</Text>
|
||||
<Text dimColor={!isSelected}>
|
||||
{' '}
|
||||
<Text backgroundColor="userMessageBackground">Plugin</Text>
|
||||
</Text>
|
||||
<Text dimColor> · {item.marketplace}</Text>
|
||||
<Text dimColor={!isSelected}> · {statusIcon} </Text>
|
||||
<Text dimColor={!isSelected}>{statusText}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (item.type === 'flagged-plugin') {
|
||||
const statusIcon = color('warning', theme)(figures.warning)
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
||||
{isSelected ? `${figures.pointer} ` : ' '}
|
||||
</Text>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>{item.name}</Text>
|
||||
<Text dimColor={!isSelected}>
|
||||
{' '}
|
||||
<Text backgroundColor="userMessageBackground">Plugin</Text>
|
||||
</Text>
|
||||
<Text dimColor> · {item.marketplace}</Text>
|
||||
<Text dimColor={!isSelected}> · {statusIcon} </Text>
|
||||
<Text dimColor={!isSelected}>removed</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (item.type === 'failed-plugin') {
|
||||
const statusIcon = color('error', theme)(figures.cross)
|
||||
const statusText = `failed to load · ${item.errorCount} ${plural(item.errorCount, 'error')}`
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
||||
{isSelected ? `${figures.pointer} ` : ' '}
|
||||
</Text>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>{item.name}</Text>
|
||||
<Text dimColor={!isSelected}>
|
||||
{' '}
|
||||
<Text backgroundColor="userMessageBackground">Plugin</Text>
|
||||
</Text>
|
||||
<Text dimColor> · {item.marketplace}</Text>
|
||||
<Text dimColor={!isSelected}> · {statusIcon} </Text>
|
||||
<Text dimColor={!isSelected}>{statusText}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// MCP server
|
||||
let statusIcon: string
|
||||
let statusText: string
|
||||
|
||||
if (item.status === 'connected') {
|
||||
statusIcon = color('success', theme)(figures.tick)
|
||||
statusText = 'connected'
|
||||
} else if (item.status === 'disabled') {
|
||||
statusIcon = color('inactive', theme)(figures.radioOff)
|
||||
statusText = 'disabled'
|
||||
} else if (item.status === 'pending') {
|
||||
statusIcon = color('inactive', theme)(figures.radioOff)
|
||||
statusText = 'connecting…'
|
||||
} else if (item.status === 'needs-auth') {
|
||||
statusIcon = color('warning', theme)(figures.triangleUpOutline)
|
||||
statusText = 'Enter to auth'
|
||||
} else {
|
||||
statusIcon = color('error', theme)(figures.cross)
|
||||
statusText = 'failed'
|
||||
}
|
||||
|
||||
// Indented MCPs (child of a plugin)
|
||||
if (item.indented) {
|
||||
return (
|
||||
<Box>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
||||
{isSelected ? `${figures.pointer} ` : ' '}
|
||||
</Text>
|
||||
<Text dimColor={!isSelected}>└ </Text>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>{item.name}</Text>
|
||||
<Text dimColor={!isSelected}>
|
||||
{' '}
|
||||
<Text backgroundColor="userMessageBackground">MCP</Text>
|
||||
</Text>
|
||||
<Text dimColor={!isSelected}> · {statusIcon} </Text>
|
||||
<Text dimColor={!isSelected}>{statusText}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
||||
{isSelected ? `${figures.pointer} ` : ' '}
|
||||
</Text>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>{item.name}</Text>
|
||||
<Text dimColor={!isSelected}>
|
||||
{' '}
|
||||
<Text backgroundColor="userMessageBackground">MCP</Text>
|
||||
</Text>
|
||||
<Text dimColor={!isSelected}> · {statusIcon} </Text>
|
||||
<Text dimColor={!isSelected}>{statusText}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,97 +1,103 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { errorMessage } from '../../utils/errors.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import { validateManifest } from '../../utils/plugins/validatePlugin.js';
|
||||
import { plural } from '../../utils/stringUtils.js';
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { errorMessage } from '../../utils/errors.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import { validateManifest } from '../../utils/plugins/validatePlugin.js'
|
||||
import { plural } from '../../utils/stringUtils.js'
|
||||
|
||||
type Props = {
|
||||
onComplete: (result?: string) => void;
|
||||
path?: string;
|
||||
};
|
||||
export function ValidatePlugin(t0) {
|
||||
const $ = _c(5);
|
||||
const {
|
||||
onComplete,
|
||||
path
|
||||
} = t0;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== onComplete || $[1] !== path) {
|
||||
t1 = () => {
|
||||
const runValidation = async function runValidation() {
|
||||
if (!path) {
|
||||
onComplete("Usage: /plugin validate <path>\n\nValidate a plugin or marketplace manifest file or directory.\n\nExamples:\n /plugin validate .claude-plugin/plugin.json\n /plugin validate /path/to/plugin-directory\n /plugin validate .\n\nWhen given a directory, automatically validates .claude-plugin/marketplace.json\nor .claude-plugin/plugin.json (prefers marketplace if both exist).\n\nOr from the command line:\n claude plugin validate <path>");
|
||||
return;
|
||||
}
|
||||
;
|
||||
try {
|
||||
const result = await validateManifest(path);
|
||||
let output = "";
|
||||
output = output + `Validating ${result.fileType} manifest: ${result.filePath}\n\n`;
|
||||
output;
|
||||
if (result.errors.length > 0) {
|
||||
output = output + `${figures.cross} Found ${result.errors.length} ${plural(result.errors.length, "error")}:\n\n`;
|
||||
output;
|
||||
result.errors.forEach(error_0 => {
|
||||
output = output + ` ${figures.pointer} ${error_0.path}: ${error_0.message}\n`;
|
||||
output;
|
||||
});
|
||||
output = output + "\n";
|
||||
output;
|
||||
}
|
||||
if (result.warnings.length > 0) {
|
||||
output = output + `${figures.warning} Found ${result.warnings.length} ${plural(result.warnings.length, "warning")}:\n\n`;
|
||||
output;
|
||||
result.warnings.forEach(warning => {
|
||||
output = output + ` ${figures.pointer} ${warning.path}: ${warning.message}\n`;
|
||||
output;
|
||||
});
|
||||
output = output + "\n";
|
||||
output;
|
||||
}
|
||||
if (result.success) {
|
||||
if (result.warnings.length > 0) {
|
||||
output = output + `${figures.tick} Validation passed with warnings\n`;
|
||||
output;
|
||||
} else {
|
||||
output = output + `${figures.tick} Validation passed\n`;
|
||||
output;
|
||||
}
|
||||
process.exitCode = 0;
|
||||
} else {
|
||||
output = output + `${figures.cross} Validation failed\n`;
|
||||
output;
|
||||
process.exitCode = 1;
|
||||
}
|
||||
onComplete(output);
|
||||
} catch (t3) {
|
||||
const error = t3;
|
||||
process.exitCode = 2;
|
||||
logError(error);
|
||||
onComplete(`${figures.cross} Unexpected error during validation: ${errorMessage(error)}`);
|
||||
}
|
||||
};
|
||||
runValidation();
|
||||
};
|
||||
t2 = [onComplete, path];
|
||||
$[0] = onComplete;
|
||||
$[1] = path;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Box flexDirection="column"><Text>Running validation...</Text></Box>;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
onComplete: (result?: string) => void
|
||||
path?: string
|
||||
}
|
||||
|
||||
export function ValidatePlugin({ onComplete, path }: Props): React.ReactNode {
|
||||
useEffect(() => {
|
||||
async function runValidation() {
|
||||
// If no path provided, show usage
|
||||
if (!path) {
|
||||
onComplete(
|
||||
'Usage: /plugin validate <path>\n\n' +
|
||||
'Validate a plugin or marketplace manifest file or directory.\n\n' +
|
||||
'Examples:\n' +
|
||||
' /plugin validate .claude-plugin/plugin.json\n' +
|
||||
' /plugin validate /path/to/plugin-directory\n' +
|
||||
' /plugin validate .\n\n' +
|
||||
'When given a directory, automatically validates .claude-plugin/marketplace.json\n' +
|
||||
'or .claude-plugin/plugin.json (prefers marketplace if both exist).\n\n' +
|
||||
'Or from the command line:\n' +
|
||||
' claude plugin validate <path>',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await validateManifest(path)
|
||||
|
||||
let output = ''
|
||||
|
||||
// Add header
|
||||
output += `Validating ${result.fileType} manifest: ${result.filePath}\n\n`
|
||||
|
||||
// Show errors
|
||||
if (result.errors.length > 0) {
|
||||
output += `${figures.cross} Found ${result.errors.length} ${plural(result.errors.length, 'error')}:\n\n`
|
||||
|
||||
result.errors.forEach(error => {
|
||||
output += ` ${figures.pointer} ${error.path}: ${error.message}\n`
|
||||
})
|
||||
|
||||
output += '\n'
|
||||
}
|
||||
|
||||
// Show warnings
|
||||
if (result.warnings.length > 0) {
|
||||
output += `${figures.warning} Found ${result.warnings.length} ${plural(result.warnings.length, 'warning')}:\n\n`
|
||||
|
||||
result.warnings.forEach(warning => {
|
||||
output += ` ${figures.pointer} ${warning.path}: ${warning.message}\n`
|
||||
})
|
||||
|
||||
output += '\n'
|
||||
}
|
||||
|
||||
// Show success or failure
|
||||
if (result.success) {
|
||||
if (result.warnings.length > 0) {
|
||||
output += `${figures.tick} Validation passed with warnings\n`
|
||||
} else {
|
||||
output += `${figures.tick} Validation passed\n`
|
||||
}
|
||||
|
||||
// Exit with code 0 (success)
|
||||
process.exitCode = 0
|
||||
} else {
|
||||
output += `${figures.cross} Validation failed\n`
|
||||
|
||||
// Exit with code 1 (validation failure)
|
||||
process.exitCode = 1
|
||||
}
|
||||
|
||||
onComplete(output)
|
||||
} catch (error) {
|
||||
// Exit with code 2 (unexpected error)
|
||||
process.exitCode = 2
|
||||
|
||||
logError(error)
|
||||
|
||||
onComplete(
|
||||
`${figures.cross} Unexpected error during validation: ${errorMessage(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
void runValidation()
|
||||
}, [onComplete, path])
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text>Running validation...</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { Command } from '../../commands.js';
|
||||
import type { Command } from '../../commands.js'
|
||||
|
||||
const plugin = {
|
||||
type: 'local-jsx',
|
||||
name: 'plugin',
|
||||
aliases: ['plugins', 'marketplace'],
|
||||
description: 'Manage Claude Code plugins',
|
||||
immediate: true,
|
||||
load: () => import('./plugin.js')
|
||||
} satisfies Command;
|
||||
export default plugin;
|
||||
load: () => import('./plugin.js'),
|
||||
} satisfies Command
|
||||
|
||||
export default plugin
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { PluginSettings } from './PluginSettings.js';
|
||||
export async function call(onDone: LocalJSXCommandOnDone, _context: unknown, args?: string): Promise<React.ReactNode> {
|
||||
return <PluginSettings onComplete={onDone} args={args} />;
|
||||
import * as React from 'react'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import { PluginSettings } from './PluginSettings.js'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
_context: unknown,
|
||||
args?: string,
|
||||
): Promise<React.ReactNode> {
|
||||
return <PluginSettings onComplete={onDone} args={args} />
|
||||
}
|
||||
|
||||
@@ -1,116 +1,123 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
/**
|
||||
* Shared helper functions and types for plugin details views
|
||||
*
|
||||
* Used by both DiscoverPlugins and BrowseMarketplace components.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js';
|
||||
import { Byline } from '../../components/design-system/Byline.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { PluginMarketplaceEntry } from '../../utils/plugins/schemas.js';
|
||||
import * as React from 'react'
|
||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
|
||||
import { Byline } from '../../components/design-system/Byline.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import type { PluginMarketplaceEntry } from '../../utils/plugins/schemas.js'
|
||||
|
||||
/**
|
||||
* Represents a plugin available for installation from a marketplace
|
||||
*/
|
||||
export type InstallablePlugin = {
|
||||
entry: PluginMarketplaceEntry;
|
||||
marketplaceName: string;
|
||||
pluginId: string;
|
||||
isInstalled: boolean;
|
||||
};
|
||||
entry: PluginMarketplaceEntry
|
||||
marketplaceName: string
|
||||
pluginId: string
|
||||
isInstalled: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu option for plugin details view
|
||||
*/
|
||||
export type PluginDetailsMenuOption = {
|
||||
label: string;
|
||||
action: string;
|
||||
};
|
||||
label: string
|
||||
action: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract GitHub repo info from a plugin's source
|
||||
*/
|
||||
export function extractGitHubRepo(plugin: InstallablePlugin): string | null {
|
||||
const isGitHub = plugin.entry.source && typeof plugin.entry.source === 'object' && 'source' in plugin.entry.source && plugin.entry.source.source === 'github';
|
||||
if (isGitHub && typeof plugin.entry.source === 'object' && 'repo' in plugin.entry.source) {
|
||||
return plugin.entry.source.repo;
|
||||
const isGitHub =
|
||||
plugin.entry.source &&
|
||||
typeof plugin.entry.source === 'object' &&
|
||||
'source' in plugin.entry.source &&
|
||||
plugin.entry.source.source === 'github'
|
||||
|
||||
if (
|
||||
isGitHub &&
|
||||
typeof plugin.entry.source === 'object' &&
|
||||
'repo' in plugin.entry.source
|
||||
) {
|
||||
return plugin.entry.source.repo
|
||||
}
|
||||
return null;
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Build menu options for plugin details view with scoped installation options
|
||||
*/
|
||||
export function buildPluginDetailsMenuOptions(hasHomepage: string | undefined, githubRepo: string | null): PluginDetailsMenuOption[] {
|
||||
const options: PluginDetailsMenuOption[] = [{
|
||||
label: 'Install for you (user scope)',
|
||||
action: 'install-user'
|
||||
}, {
|
||||
label: 'Install for all collaborators on this repository (project scope)',
|
||||
action: 'install-project'
|
||||
}, {
|
||||
label: 'Install for you, in this repo only (local scope)',
|
||||
action: 'install-local'
|
||||
}];
|
||||
export function buildPluginDetailsMenuOptions(
|
||||
hasHomepage: string | undefined,
|
||||
githubRepo: string | null,
|
||||
): PluginDetailsMenuOption[] {
|
||||
const options: PluginDetailsMenuOption[] = [
|
||||
{ label: 'Install for you (user scope)', action: 'install-user' },
|
||||
{
|
||||
label: 'Install for all collaborators on this repository (project scope)',
|
||||
action: 'install-project',
|
||||
},
|
||||
{
|
||||
label: 'Install for you, in this repo only (local scope)',
|
||||
action: 'install-local',
|
||||
},
|
||||
]
|
||||
if (hasHomepage) {
|
||||
options.push({
|
||||
label: 'Open homepage',
|
||||
action: 'homepage'
|
||||
});
|
||||
options.push({ label: 'Open homepage', action: 'homepage' })
|
||||
}
|
||||
if (githubRepo) {
|
||||
options.push({
|
||||
label: 'View on GitHub',
|
||||
action: 'github'
|
||||
});
|
||||
options.push({ label: 'View on GitHub', action: 'github' })
|
||||
}
|
||||
options.push({
|
||||
label: 'Back to plugin list',
|
||||
action: 'back'
|
||||
});
|
||||
return options;
|
||||
options.push({ label: 'Back to plugin list', action: 'back' })
|
||||
return options
|
||||
}
|
||||
|
||||
/**
|
||||
* Key hint component for plugin selection screens
|
||||
*/
|
||||
export function PluginSelectionKeyHint(t0) {
|
||||
const $ = _c(7);
|
||||
const {
|
||||
hasSelection
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== hasSelection) {
|
||||
t1 = hasSelection && <ConfigurableShortcutHint action="plugin:install" context="Plugin" fallback="i" description="install" bold={true} />;
|
||||
$[0] = hasSelection;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
let t3;
|
||||
let t4;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <ConfigurableShortcutHint action="plugin:toggle" context="Plugin" fallback="Space" description="toggle" />;
|
||||
t3 = <ConfigurableShortcutHint action="select:accept" context="Select" fallback="Enter" description="details" />;
|
||||
t4 = <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="back" />;
|
||||
$[2] = t2;
|
||||
$[3] = t3;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
t3 = $[3];
|
||||
t4 = $[4];
|
||||
}
|
||||
let t5;
|
||||
if ($[5] !== t1) {
|
||||
t5 = <Box marginTop={1}><Text dimColor={true} italic={true}><Byline>{t1}{t2}{t3}{t4}</Byline></Text></Box>;
|
||||
$[5] = t1;
|
||||
$[6] = t5;
|
||||
} else {
|
||||
t5 = $[6];
|
||||
}
|
||||
return t5;
|
||||
export function PluginSelectionKeyHint({
|
||||
hasSelection,
|
||||
}: {
|
||||
hasSelection: boolean
|
||||
}): React.ReactNode {
|
||||
return (
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor italic>
|
||||
<Byline>
|
||||
{hasSelection && (
|
||||
<ConfigurableShortcutHint
|
||||
action="plugin:install"
|
||||
context="Plugin"
|
||||
fallback="i"
|
||||
description="install"
|
||||
bold
|
||||
/>
|
||||
)}
|
||||
<ConfigurableShortcutHint
|
||||
action="plugin:toggle"
|
||||
context="Plugin"
|
||||
fallback="Space"
|
||||
description="toggle"
|
||||
/>
|
||||
<ConfigurableShortcutHint
|
||||
action="select:accept"
|
||||
context="Select"
|
||||
fallback="Enter"
|
||||
description="details"
|
||||
/>
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="back"
|
||||
/>
|
||||
</Byline>
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,57 +1,96 @@
|
||||
import * as React from 'react';
|
||||
import { type GroveDecision, GroveDialog, PrivacySettingsDialog } from '../../components/grove/Grove.js';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js';
|
||||
import { getGroveNoticeConfig, getGroveSettings, isQualifiedForGrove } from '../../services/api/grove.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
const FALLBACK_MESSAGE = 'Review and manage your privacy settings at https://claude.ai/settings/data-privacy-controls';
|
||||
export async function call(onDone: LocalJSXCommandOnDone): Promise<React.ReactNode | null> {
|
||||
const qualified = await isQualifiedForGrove();
|
||||
import * as React from 'react'
|
||||
import {
|
||||
type GroveDecision,
|
||||
GroveDialog,
|
||||
PrivacySettingsDialog,
|
||||
} from '../../components/grove/Grove.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
import {
|
||||
getGroveNoticeConfig,
|
||||
getGroveSettings,
|
||||
isQualifiedForGrove,
|
||||
} from '../../services/api/grove.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
|
||||
const FALLBACK_MESSAGE =
|
||||
'Review and manage your privacy settings at https://claude.ai/settings/data-privacy-controls'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
): Promise<React.ReactNode | null> {
|
||||
const qualified = await isQualifiedForGrove()
|
||||
if (!qualified) {
|
||||
onDone(FALLBACK_MESSAGE);
|
||||
return null;
|
||||
onDone(FALLBACK_MESSAGE)
|
||||
return null
|
||||
}
|
||||
const [settingsResult, configResult] = await Promise.all([getGroveSettings(), getGroveNoticeConfig()]);
|
||||
|
||||
const [settingsResult, configResult] = await Promise.all([
|
||||
getGroveSettings(),
|
||||
getGroveNoticeConfig(),
|
||||
])
|
||||
// Hide dialog on API failure (after retry)
|
||||
if (!settingsResult.success) {
|
||||
onDone(FALLBACK_MESSAGE);
|
||||
return null;
|
||||
onDone(FALLBACK_MESSAGE)
|
||||
return null
|
||||
}
|
||||
const settings = settingsResult.data;
|
||||
const config = configResult.success ? configResult.data : null;
|
||||
const settings = settingsResult.data
|
||||
const config = configResult.success ? configResult.data : null
|
||||
|
||||
async function onDoneWithDecision(decision: GroveDecision) {
|
||||
if (decision === 'escape' || decision === 'defer') {
|
||||
onDone('Privacy settings dialog dismissed', {
|
||||
display: 'system'
|
||||
});
|
||||
return;
|
||||
display: 'system',
|
||||
})
|
||||
return
|
||||
}
|
||||
await onDoneWithSettingsCheck();
|
||||
await onDoneWithSettingsCheck()
|
||||
}
|
||||
|
||||
async function onDoneWithSettingsCheck() {
|
||||
const updatedSettingsResult = await getGroveSettings();
|
||||
const updatedSettingsResult = await getGroveSettings()
|
||||
if (!updatedSettingsResult.success) {
|
||||
onDone('Unable to retrieve updated privacy settings', {
|
||||
display: 'system'
|
||||
});
|
||||
return;
|
||||
display: 'system',
|
||||
})
|
||||
return
|
||||
}
|
||||
const updatedSettings = updatedSettingsResult.data;
|
||||
const groveStatus = updatedSettings.grove_enabled ? 'true' : 'false';
|
||||
onDone(`"Help improve Claude" set to ${groveStatus}.`);
|
||||
if (settings.grove_enabled !== null && settings.grove_enabled !== updatedSettings.grove_enabled) {
|
||||
const updatedSettings = updatedSettingsResult.data
|
||||
const groveStatus = updatedSettings.grove_enabled ? 'true' : 'false'
|
||||
onDone(`"Help improve Claude" set to ${groveStatus}.`)
|
||||
if (
|
||||
settings.grove_enabled !== null &&
|
||||
settings.grove_enabled !== updatedSettings.grove_enabled
|
||||
) {
|
||||
logEvent('tengu_grove_policy_toggled', {
|
||||
state: updatedSettings.grove_enabled as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
location: 'settings' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
state:
|
||||
updatedSettings.grove_enabled as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
location:
|
||||
'settings' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Show privacy settings directly if the user has already accepted the
|
||||
// terms.
|
||||
if (settings.grove_enabled !== null) {
|
||||
return <PrivacySettingsDialog settings={settings} domainExcluded={config?.domain_excluded} onDone={onDoneWithSettingsCheck}></PrivacySettingsDialog>;
|
||||
return (
|
||||
<PrivacySettingsDialog
|
||||
settings={settings}
|
||||
domainExcluded={config?.domain_excluded}
|
||||
onDone={onDoneWithSettingsCheck}
|
||||
></PrivacySettingsDialog>
|
||||
)
|
||||
}
|
||||
|
||||
// Show the GroveDialog for users who haven't accepted terms yet
|
||||
return <GroveDialog showIfAlreadyViewed={true} onDone={onDoneWithDecision} location={'settings'} />;
|
||||
return (
|
||||
<GroveDialog
|
||||
showIfAlreadyViewed={true}
|
||||
onDone={onDoneWithDecision}
|
||||
location={'settings'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,209 +1,177 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import type { CommandResultDisplay, LocalJSXCommandContext } from '../../commands.js';
|
||||
import { type OptionWithDescription, Select } from '../../components/CustomSelect/select.js';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js';
|
||||
import { logEvent } from '../../services/analytics/index.js';
|
||||
import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js';
|
||||
import type { ToolUseContext } from '../../Tool.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { getOauthAccountInfo, getRateLimitTier, getSubscriptionType } from '../../utils/auth.js';
|
||||
import { hasClaudeAiBillingAccess } from '../../utils/billing.js';
|
||||
import { call as extraUsageCall } from '../extra-usage/extra-usage.js';
|
||||
import { extraUsage } from '../extra-usage/index.js';
|
||||
import upgrade from '../upgrade/index.js';
|
||||
import { call as upgradeCall } from '../upgrade/upgrade.js';
|
||||
type RateLimitOptionsMenuOptionType = 'upgrade' | 'extra-usage' | 'cancel';
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import type {
|
||||
CommandResultDisplay,
|
||||
LocalJSXCommandContext,
|
||||
} from '../../commands.js'
|
||||
import {
|
||||
type OptionWithDescription,
|
||||
Select,
|
||||
} from '../../components/CustomSelect/select.js'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
|
||||
import { logEvent } from '../../services/analytics/index.js'
|
||||
import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js'
|
||||
import type { ToolUseContext } from '../../Tool.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import {
|
||||
getOauthAccountInfo,
|
||||
getRateLimitTier,
|
||||
getSubscriptionType,
|
||||
} from '../../utils/auth.js'
|
||||
import { hasClaudeAiBillingAccess } from '../../utils/billing.js'
|
||||
import { call as extraUsageCall } from '../extra-usage/extra-usage.js'
|
||||
import { extraUsage } from '../extra-usage/index.js'
|
||||
import upgrade from '../upgrade/index.js'
|
||||
import { call as upgradeCall } from '../upgrade/upgrade.js'
|
||||
|
||||
type RateLimitOptionsMenuOptionType = 'upgrade' | 'extra-usage' | 'cancel'
|
||||
|
||||
type RateLimitOptionsMenuProps = {
|
||||
onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay | undefined;
|
||||
} | undefined) => void;
|
||||
context: ToolUseContext & LocalJSXCommandContext;
|
||||
};
|
||||
function RateLimitOptionsMenu(t0) {
|
||||
const $ = _c(25);
|
||||
const {
|
||||
onDone,
|
||||
context
|
||||
} = t0;
|
||||
const [subCommandJSX, setSubCommandJSX] = useState(null);
|
||||
const claudeAiLimits = useClaudeAiLimits();
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = getSubscriptionType();
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const subscriptionType = t1;
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = getRateLimitTier();
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const rateLimitTier = t2;
|
||||
const hasExtraUsageEnabled = getOauthAccountInfo()?.hasExtraUsageEnabled === true;
|
||||
const isMax = subscriptionType === "max";
|
||||
const isMax20x = isMax && rateLimitTier === "default_claude_max_20x";
|
||||
const isTeamOrEnterprise = subscriptionType === "team" || subscriptionType === "enterprise";
|
||||
const buyFirst = getFeatureValue_CACHED_MAY_BE_STALE("tengu_jade_anvil_4", false);
|
||||
let t3;
|
||||
bb0: {
|
||||
let actionOptions;
|
||||
if ($[2] !== claudeAiLimits.overageDisabledReason || $[3] !== claudeAiLimits.overageStatus) {
|
||||
actionOptions = [];
|
||||
if (extraUsage.isEnabled()) {
|
||||
const hasBillingAccess = hasClaudeAiBillingAccess();
|
||||
const needsToRequestFromAdmin = isTeamOrEnterprise && !hasBillingAccess;
|
||||
const isOrgSpendCapDepleted = claudeAiLimits.overageDisabledReason === "out_of_credits" || claudeAiLimits.overageDisabledReason === "org_level_disabled_until" || claudeAiLimits.overageDisabledReason === "org_service_zero_credit_limit";
|
||||
if (needsToRequestFromAdmin && isOrgSpendCapDepleted) {} else {
|
||||
const isOverageState = claudeAiLimits.overageStatus === "rejected" || claudeAiLimits.overageStatus === "allowed_warning";
|
||||
let label;
|
||||
if (needsToRequestFromAdmin) {
|
||||
label = isOverageState ? "Request more" : "Request extra usage";
|
||||
} else {
|
||||
label = hasExtraUsageEnabled ? "Add funds to continue with extra usage" : "Switch to extra usage";
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== label) {
|
||||
t4 = {
|
||||
label,
|
||||
value: "extra-usage"
|
||||
};
|
||||
$[5] = label;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
actionOptions.push(t4);
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?:
|
||||
| {
|
||||
display?: CommandResultDisplay | undefined
|
||||
}
|
||||
}
|
||||
if (!isMax20x && !isTeamOrEnterprise && upgrade.isEnabled()) {
|
||||
let t4;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = {
|
||||
label: "Upgrade your plan",
|
||||
value: "upgrade"
|
||||
};
|
||||
$[7] = t4;
|
||||
| undefined,
|
||||
) => void
|
||||
context: ToolUseContext & LocalJSXCommandContext
|
||||
}
|
||||
|
||||
function RateLimitOptionsMenu({
|
||||
onDone,
|
||||
context,
|
||||
}: RateLimitOptionsMenuProps): React.ReactNode {
|
||||
const [subCommandJSX, setSubCommandJSX] = useState<React.ReactNode>(null)
|
||||
const claudeAiLimits = useClaudeAiLimits()
|
||||
const subscriptionType = getSubscriptionType()
|
||||
const rateLimitTier = getRateLimitTier()
|
||||
const hasExtraUsageEnabled =
|
||||
getOauthAccountInfo()?.hasExtraUsageEnabled === true
|
||||
const isMax = subscriptionType === 'max'
|
||||
const isMax20x = isMax && rateLimitTier === 'default_claude_max_20x'
|
||||
const isTeamOrEnterprise =
|
||||
subscriptionType === 'team' || subscriptionType === 'enterprise'
|
||||
const buyFirst = getFeatureValue_CACHED_MAY_BE_STALE(
|
||||
'tengu_jade_anvil_4',
|
||||
false,
|
||||
)
|
||||
|
||||
const options = useMemo<
|
||||
OptionWithDescription<RateLimitOptionsMenuOptionType>[]
|
||||
>(() => {
|
||||
const actionOptions: OptionWithDescription<RateLimitOptionsMenuOptionType>[] =
|
||||
[]
|
||||
|
||||
if (extraUsage.isEnabled()) {
|
||||
const hasBillingAccess = hasClaudeAiBillingAccess()
|
||||
const needsToRequestFromAdmin = isTeamOrEnterprise && !hasBillingAccess
|
||||
// Org spend cap depleted - non-admins can't request more since there's nothing to allocate
|
||||
// - out_of_credits: wallet empty
|
||||
// - org_level_disabled_until: org spend cap hit for the month
|
||||
// - org_service_zero_credit_limit: org service has zero credit limit
|
||||
const isOrgSpendCapDepleted =
|
||||
claudeAiLimits.overageDisabledReason === 'out_of_credits' ||
|
||||
claudeAiLimits.overageDisabledReason === 'org_level_disabled_until' ||
|
||||
claudeAiLimits.overageDisabledReason === 'org_service_zero_credit_limit'
|
||||
|
||||
// Hide for non-admin Team/Enterprise users when org spend cap is depleted
|
||||
if (needsToRequestFromAdmin && isOrgSpendCapDepleted) {
|
||||
// Don't show extra-usage option
|
||||
} else {
|
||||
const isOverageState =
|
||||
claudeAiLimits.overageStatus === 'rejected' ||
|
||||
claudeAiLimits.overageStatus === 'allowed_warning'
|
||||
|
||||
let label: string
|
||||
if (needsToRequestFromAdmin) {
|
||||
label = isOverageState ? 'Request more' : 'Request extra usage'
|
||||
} else {
|
||||
t4 = $[7];
|
||||
label = hasExtraUsageEnabled
|
||||
? 'Add funds to continue with extra usage'
|
||||
: 'Switch to extra usage'
|
||||
}
|
||||
actionOptions.push(t4);
|
||||
|
||||
actionOptions.push({
|
||||
label,
|
||||
value: 'extra-usage',
|
||||
})
|
||||
}
|
||||
$[2] = claudeAiLimits.overageDisabledReason;
|
||||
$[3] = claudeAiLimits.overageStatus;
|
||||
$[4] = actionOptions;
|
||||
} else {
|
||||
actionOptions = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = {
|
||||
label: "Stop and wait for limit to reset",
|
||||
value: "cancel"
|
||||
};
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
|
||||
if (!isMax20x && !isTeamOrEnterprise && upgrade.isEnabled()) {
|
||||
actionOptions.push({
|
||||
label: 'Upgrade your plan',
|
||||
value: 'upgrade',
|
||||
})
|
||||
}
|
||||
const cancelOption = t4;
|
||||
|
||||
const cancelOption: OptionWithDescription<RateLimitOptionsMenuOptionType> =
|
||||
{
|
||||
label: 'Stop and wait for limit to reset',
|
||||
value: 'cancel',
|
||||
}
|
||||
|
||||
if (buyFirst) {
|
||||
let t5;
|
||||
if ($[9] !== actionOptions) {
|
||||
t5 = [...actionOptions, cancelOption];
|
||||
$[9] = actionOptions;
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
}
|
||||
t3 = t5;
|
||||
break bb0;
|
||||
return [...actionOptions, cancelOption]
|
||||
}
|
||||
let t5;
|
||||
if ($[11] !== actionOptions) {
|
||||
t5 = [cancelOption, ...actionOptions];
|
||||
$[11] = actionOptions;
|
||||
$[12] = t5;
|
||||
} else {
|
||||
t5 = $[12];
|
||||
}
|
||||
t3 = t5;
|
||||
return [cancelOption, ...actionOptions]
|
||||
}, [
|
||||
buyFirst,
|
||||
isMax20x,
|
||||
isTeamOrEnterprise,
|
||||
hasExtraUsageEnabled,
|
||||
claudeAiLimits.overageStatus,
|
||||
claudeAiLimits.overageDisabledReason,
|
||||
])
|
||||
|
||||
function handleCancel(): void {
|
||||
logEvent('tengu_rate_limit_options_menu_cancel', {})
|
||||
onDone(undefined, { display: 'skip' })
|
||||
}
|
||||
const options = t3;
|
||||
let t4;
|
||||
if ($[13] !== onDone) {
|
||||
t4 = function handleCancel() {
|
||||
logEvent("tengu_rate_limit_options_menu_cancel", {});
|
||||
onDone(undefined, {
|
||||
display: "skip"
|
||||
});
|
||||
};
|
||||
$[13] = onDone;
|
||||
$[14] = t4;
|
||||
} else {
|
||||
t4 = $[14];
|
||||
}
|
||||
const handleCancel = t4;
|
||||
let t5;
|
||||
if ($[15] !== context || $[16] !== handleCancel || $[17] !== onDone) {
|
||||
t5 = function handleSelect(value) {
|
||||
if (value === "upgrade") {
|
||||
logEvent("tengu_rate_limit_options_menu_select_upgrade", {});
|
||||
upgradeCall(onDone, context).then(jsx => {
|
||||
if (jsx) {
|
||||
setSubCommandJSX(jsx);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (value === "extra-usage") {
|
||||
logEvent("tengu_rate_limit_options_menu_select_extra_usage", {});
|
||||
extraUsageCall(onDone, context).then(jsx_0 => {
|
||||
if (jsx_0) {
|
||||
setSubCommandJSX(jsx_0);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (value === "cancel") {
|
||||
handleCancel();
|
||||
}
|
||||
|
||||
function handleSelect(value: RateLimitOptionsMenuOptionType): void {
|
||||
if (value === 'upgrade') {
|
||||
logEvent('tengu_rate_limit_options_menu_select_upgrade', {})
|
||||
void upgradeCall(onDone, context).then(jsx => {
|
||||
if (jsx) {
|
||||
setSubCommandJSX(jsx)
|
||||
}
|
||||
}
|
||||
};
|
||||
$[15] = context;
|
||||
$[16] = handleCancel;
|
||||
$[17] = onDone;
|
||||
$[18] = t5;
|
||||
} else {
|
||||
t5 = $[18];
|
||||
})
|
||||
} else if (value === 'extra-usage') {
|
||||
logEvent('tengu_rate_limit_options_menu_select_extra_usage', {})
|
||||
void extraUsageCall(onDone, context).then(jsx => {
|
||||
if (jsx) {
|
||||
setSubCommandJSX(jsx)
|
||||
}
|
||||
})
|
||||
} else if (value === 'cancel') {
|
||||
handleCancel()
|
||||
}
|
||||
}
|
||||
const handleSelect = t5;
|
||||
|
||||
if (subCommandJSX) {
|
||||
return subCommandJSX;
|
||||
return subCommandJSX
|
||||
}
|
||||
let t6;
|
||||
if ($[19] !== handleSelect || $[20] !== options) {
|
||||
t6 = <Select options={options} onChange={handleSelect} visibleOptionCount={options.length} />;
|
||||
$[19] = handleSelect;
|
||||
$[20] = options;
|
||||
$[21] = t6;
|
||||
} else {
|
||||
t6 = $[21];
|
||||
}
|
||||
let t7;
|
||||
if ($[22] !== handleCancel || $[23] !== t6) {
|
||||
t7 = <Dialog title="What do you want to do?" onCancel={handleCancel} color="suggestion">{t6}</Dialog>;
|
||||
$[22] = handleCancel;
|
||||
$[23] = t6;
|
||||
$[24] = t7;
|
||||
} else {
|
||||
t7 = $[24];
|
||||
}
|
||||
return t7;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="What do you want to do?"
|
||||
onCancel={handleCancel}
|
||||
color="suggestion"
|
||||
>
|
||||
<Select<RateLimitOptionsMenuOptionType>
|
||||
options={options}
|
||||
onChange={handleSelect}
|
||||
visibleOptionCount={options.length}
|
||||
/>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: ToolUseContext & LocalJSXCommandContext): Promise<React.ReactNode> {
|
||||
return <RateLimitOptionsMenu onDone={onDone} context={context} />;
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: ToolUseContext & LocalJSXCommandContext,
|
||||
): Promise<React.ReactNode> {
|
||||
return <RateLimitOptionsMenu onDone={onDone} context={context} />
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { RemoteEnvironmentDialog } from '../../components/RemoteEnvironmentDialog.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
export async function call(onDone: LocalJSXCommandOnDone): Promise<React.ReactNode> {
|
||||
return <RemoteEnvironmentDialog onDone={onDone} />;
|
||||
import * as React from 'react'
|
||||
import { RemoteEnvironmentDialog } from '../../components/RemoteEnvironmentDialog.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
): Promise<React.ReactNode> {
|
||||
return <RemoteEnvironmentDialog onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,163 +1,162 @@
|
||||
import { execa } from 'execa';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Select } from '../../components/CustomSelect/index.js';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { LoadingState } from '../../components/design-system/LoadingState.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { logEvent, type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS as SafeString } from '../../services/analytics/index.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { openBrowser } from '../../utils/browser.js';
|
||||
import { getGhAuthStatus } from '../../utils/github/ghAuthStatus.js';
|
||||
import { createDefaultEnvironment, getCodeWebUrl, type ImportTokenError, importGithubToken, isSignedIn, RedactedGithubToken } from './api.js';
|
||||
type CheckResult = {
|
||||
status: 'not_signed_in';
|
||||
} | {
|
||||
status: 'has_gh_token';
|
||||
token: RedactedGithubToken;
|
||||
} | {
|
||||
status: 'gh_not_installed';
|
||||
} | {
|
||||
status: 'gh_not_authenticated';
|
||||
};
|
||||
import { execa } from 'execa'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Select } from '../../components/CustomSelect/index.js'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { LoadingState } from '../../components/design-system/LoadingState.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import {
|
||||
logEvent,
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS as SafeString,
|
||||
} from '../../services/analytics/index.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import { openBrowser } from '../../utils/browser.js'
|
||||
import { getGhAuthStatus } from '../../utils/github/ghAuthStatus.js'
|
||||
import {
|
||||
createDefaultEnvironment,
|
||||
getCodeWebUrl,
|
||||
type ImportTokenError,
|
||||
importGithubToken,
|
||||
isSignedIn,
|
||||
RedactedGithubToken,
|
||||
} from './api.js'
|
||||
|
||||
type CheckResult =
|
||||
| { status: 'not_signed_in' }
|
||||
| { status: 'has_gh_token'; token: RedactedGithubToken }
|
||||
| { status: 'gh_not_installed' }
|
||||
| { status: 'gh_not_authenticated' }
|
||||
|
||||
async function checkLoginState(): Promise<CheckResult> {
|
||||
if (!(await isSignedIn())) {
|
||||
return {
|
||||
status: 'not_signed_in'
|
||||
};
|
||||
return { status: 'not_signed_in' }
|
||||
}
|
||||
const ghStatus = await getGhAuthStatus();
|
||||
|
||||
const ghStatus = await getGhAuthStatus()
|
||||
if (ghStatus === 'not_installed') {
|
||||
return {
|
||||
status: 'gh_not_installed'
|
||||
};
|
||||
return { status: 'gh_not_installed' }
|
||||
}
|
||||
if (ghStatus === 'not_authenticated') {
|
||||
return {
|
||||
status: 'gh_not_authenticated'
|
||||
};
|
||||
return { status: 'gh_not_authenticated' }
|
||||
}
|
||||
|
||||
// ghStatus === 'authenticated'. getGhAuthStatus spawns with stdout:'ignore'
|
||||
// (telemetry-safe); spawn once more with stdout:'pipe' to read the token.
|
||||
const {
|
||||
stdout
|
||||
} = await execa('gh', ['auth', 'token'], {
|
||||
const { stdout } = await execa('gh', ['auth', 'token'], {
|
||||
stdout: 'pipe',
|
||||
stderr: 'ignore',
|
||||
timeout: 5000,
|
||||
reject: false
|
||||
});
|
||||
const trimmed = stdout.trim();
|
||||
reject: false,
|
||||
})
|
||||
const trimmed = stdout.trim()
|
||||
if (!trimmed) {
|
||||
return {
|
||||
status: 'gh_not_authenticated'
|
||||
};
|
||||
return { status: 'gh_not_authenticated' }
|
||||
}
|
||||
return {
|
||||
status: 'has_gh_token',
|
||||
token: new RedactedGithubToken(trimmed)
|
||||
};
|
||||
return { status: 'has_gh_token', token: new RedactedGithubToken(trimmed) }
|
||||
}
|
||||
|
||||
function errorMessage(err: ImportTokenError, codeUrl: string): string {
|
||||
switch (err.kind) {
|
||||
case 'not_signed_in':
|
||||
return `Login failed. Please visit ${codeUrl} and login using the GitHub App`;
|
||||
return `Login failed. Please visit ${codeUrl} and login using the GitHub App`
|
||||
case 'invalid_token':
|
||||
return 'GitHub rejected that token. Run `gh auth login` and try again.';
|
||||
return 'GitHub rejected that token. Run `gh auth login` and try again.'
|
||||
case 'server':
|
||||
return `Server error (${err.status}). Try again in a moment.`;
|
||||
return `Server error (${err.status}). Try again in a moment.`
|
||||
case 'network':
|
||||
return "Couldn't reach the server. Check your connection.";
|
||||
return "Couldn't reach the server. Check your connection."
|
||||
}
|
||||
}
|
||||
type Step = {
|
||||
name: 'checking';
|
||||
} | {
|
||||
name: 'confirm';
|
||||
token: RedactedGithubToken;
|
||||
} | {
|
||||
name: 'uploading';
|
||||
};
|
||||
function Web({
|
||||
onDone
|
||||
}: {
|
||||
onDone: LocalJSXCommandOnDone;
|
||||
}) {
|
||||
const [step, setStep] = useState<Step>({
|
||||
name: 'checking'
|
||||
});
|
||||
|
||||
type Step =
|
||||
| { name: 'checking' }
|
||||
| { name: 'confirm'; token: RedactedGithubToken }
|
||||
| { name: 'uploading' }
|
||||
|
||||
function Web({ onDone }: { onDone: LocalJSXCommandOnDone }) {
|
||||
const [step, setStep] = useState<Step>({ name: 'checking' })
|
||||
|
||||
useEffect(() => {
|
||||
logEvent('tengu_remote_setup_started', {});
|
||||
logEvent('tengu_remote_setup_started', {})
|
||||
void checkLoginState().then(async result => {
|
||||
switch (result.status) {
|
||||
case 'not_signed_in':
|
||||
logEvent('tengu_remote_setup_result', {
|
||||
result: 'not_signed_in' as SafeString
|
||||
});
|
||||
onDone('Not signed in to Claude. Run /login first.');
|
||||
return;
|
||||
result: 'not_signed_in' as SafeString,
|
||||
})
|
||||
onDone('Not signed in to Claude. Run /login first.')
|
||||
return
|
||||
case 'gh_not_installed':
|
||||
case 'gh_not_authenticated':
|
||||
{
|
||||
const url = `${getCodeWebUrl()}/onboarding?step=alt-auth`;
|
||||
await openBrowser(url);
|
||||
logEvent('tengu_remote_setup_result', {
|
||||
result: result.status as SafeString
|
||||
});
|
||||
onDone(result.status === 'gh_not_installed' ? `GitHub CLI not found. Install it via https://cli.github.com/, then run \`gh auth login\`, or connect GitHub on the web: ${url}` : `GitHub CLI not authenticated. Run \`gh auth login\` and try again, or connect GitHub on the web: ${url}`);
|
||||
return;
|
||||
}
|
||||
case 'gh_not_authenticated': {
|
||||
const url = `${getCodeWebUrl()}/onboarding?step=alt-auth`
|
||||
await openBrowser(url)
|
||||
logEvent('tengu_remote_setup_result', {
|
||||
result: result.status as SafeString,
|
||||
})
|
||||
onDone(
|
||||
result.status === 'gh_not_installed'
|
||||
? `GitHub CLI not found. Install it via https://cli.github.com/, then run \`gh auth login\`, or connect GitHub on the web: ${url}`
|
||||
: `GitHub CLI not authenticated. Run \`gh auth login\` and try again, or connect GitHub on the web: ${url}`,
|
||||
)
|
||||
return
|
||||
}
|
||||
case 'has_gh_token':
|
||||
setStep({
|
||||
name: 'confirm',
|
||||
token: result.token
|
||||
});
|
||||
setStep({ name: 'confirm', token: result.token })
|
||||
}
|
||||
});
|
||||
})
|
||||
// onDone is stable across renders; intentionally not in deps.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
const handleCancel = () => {
|
||||
logEvent('tengu_remote_setup_result', {
|
||||
result: 'cancelled' as SafeString
|
||||
});
|
||||
onDone();
|
||||
};
|
||||
result: 'cancelled' as SafeString,
|
||||
})
|
||||
onDone()
|
||||
}
|
||||
|
||||
const handleConfirm = async (token: RedactedGithubToken) => {
|
||||
setStep({
|
||||
name: 'uploading'
|
||||
});
|
||||
const result = await importGithubToken(token);
|
||||
setStep({ name: 'uploading' })
|
||||
|
||||
const result = await importGithubToken(token)
|
||||
if (!result.ok) {
|
||||
const importErr = (result as { ok: false; error: ImportTokenError }).error;
|
||||
logEvent('tengu_remote_setup_result', {
|
||||
result: 'import_failed' as SafeString,
|
||||
error_kind: importErr.kind as SafeString
|
||||
});
|
||||
onDone(errorMessage(importErr, getCodeWebUrl()));
|
||||
return;
|
||||
error_kind: result.error.kind as SafeString,
|
||||
})
|
||||
onDone(errorMessage(result.error, getCodeWebUrl()))
|
||||
return
|
||||
}
|
||||
|
||||
// Token import succeeded. Environment creation is best-effort — if it
|
||||
// fails, the web state machine routes to env-setup on landing, which is
|
||||
// one extra click but still better than the OAuth dance.
|
||||
await createDefaultEnvironment();
|
||||
const url = getCodeWebUrl();
|
||||
await openBrowser(url);
|
||||
await createDefaultEnvironment()
|
||||
|
||||
const url = getCodeWebUrl()
|
||||
await openBrowser(url)
|
||||
|
||||
logEvent('tengu_remote_setup_result', {
|
||||
result: 'success' as SafeString
|
||||
});
|
||||
onDone(`Connected as ${result.result.github_username}. Opened ${url}`);
|
||||
};
|
||||
result: 'success' as SafeString,
|
||||
})
|
||||
onDone(`Connected as ${result.result.github_username}. Opened ${url}`)
|
||||
}
|
||||
|
||||
if (step.name === 'checking') {
|
||||
return <LoadingState message="Checking login status…" />;
|
||||
return <LoadingState message="Checking login status…" />
|
||||
}
|
||||
|
||||
if (step.name === 'uploading') {
|
||||
return <LoadingState message="Connecting GitHub to Claude…" />;
|
||||
return <LoadingState message="Connecting GitHub to Claude…" />
|
||||
}
|
||||
const token = step.token;
|
||||
return <Dialog title="Connect Claude on the web to GitHub?" onCancel={handleCancel} hideInputGuide>
|
||||
|
||||
const token = step.token
|
||||
return (
|
||||
<Dialog
|
||||
title="Connect Claude on the web to GitHub?"
|
||||
onCancel={handleCancel}
|
||||
hideInputGuide
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
<Text>
|
||||
Claude on the web requires connecting to your GitHub account to clone
|
||||
@@ -167,21 +166,26 @@ function Web({
|
||||
Your local credentials are used to authenticate with GitHub
|
||||
</Text>
|
||||
</Box>
|
||||
<Select options={[{
|
||||
label: 'Continue',
|
||||
value: 'send'
|
||||
}, {
|
||||
label: 'Cancel',
|
||||
value: 'cancel'
|
||||
}]} onChange={value => {
|
||||
if (value === 'send') {
|
||||
void handleConfirm(token);
|
||||
} else {
|
||||
handleCancel();
|
||||
}
|
||||
}} onCancel={handleCancel} />
|
||||
</Dialog>;
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'Continue', value: 'send' },
|
||||
{ label: 'Cancel', value: 'cancel' },
|
||||
]}
|
||||
onChange={value => {
|
||||
if (value === 'send') {
|
||||
void handleConfirm(token)
|
||||
} else {
|
||||
handleCancel()
|
||||
}
|
||||
}}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone): Promise<React.ReactNode> {
|
||||
return <Web onDone={onDone} />;
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
): Promise<React.ReactNode> {
|
||||
return <Web onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,257 +1,300 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import chalk from 'chalk';
|
||||
import type { UUID } from 'crypto';
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { getOriginalCwd, getSessionId } from '../../bootstrap/state.js';
|
||||
import type { CommandResultDisplay, ResumeEntrypoint } from '../../commands.js';
|
||||
import { LogSelector } from '../../components/LogSelector.js';
|
||||
import { MessageResponse } from '../../components/MessageResponse.js';
|
||||
import { Spinner } from '../../components/Spinner.js';
|
||||
import { useIsInsideModal } from '../../context/modalContext.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { setClipboard } from '../../ink/termio/osc.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import type { LogOption } from '../../types/logs.js';
|
||||
import { agenticSessionSearch } from '../../utils/agenticSessionSearch.js';
|
||||
import { checkCrossProjectResume } from '../../utils/crossProjectResume.js';
|
||||
import { getWorktreePaths } from '../../utils/getWorktreePaths.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import { getLastSessionLog, getSessionIdFromLog, isCustomTitleEnabled, isLiteLog, loadAllProjectsMessageLogs, loadFullLog, loadSameRepoMessageLogs, searchSessionsByCustomTitle } from '../../utils/sessionStorage.js';
|
||||
import { validateUuid } from '../../utils/uuid.js';
|
||||
type ResumeResult = {
|
||||
resultType: 'sessionNotFound';
|
||||
arg: string;
|
||||
} | {
|
||||
resultType: 'multipleMatches';
|
||||
arg: string;
|
||||
count: number;
|
||||
};
|
||||
import chalk from 'chalk'
|
||||
import type { UUID } from 'crypto'
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { getOriginalCwd, getSessionId } from '../../bootstrap/state.js'
|
||||
import type { CommandResultDisplay, ResumeEntrypoint } from '../../commands.js'
|
||||
import { LogSelector } from '../../components/LogSelector.js'
|
||||
import { MessageResponse } from '../../components/MessageResponse.js'
|
||||
import { Spinner } from '../../components/Spinner.js'
|
||||
import { useIsInsideModal } from '../../context/modalContext.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import { setClipboard } from '../../ink/termio/osc.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
import type { LogOption } from '../../types/logs.js'
|
||||
import { agenticSessionSearch } from '../../utils/agenticSessionSearch.js'
|
||||
import { checkCrossProjectResume } from '../../utils/crossProjectResume.js'
|
||||
import { getWorktreePaths } from '../../utils/getWorktreePaths.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import {
|
||||
getLastSessionLog,
|
||||
getSessionIdFromLog,
|
||||
isCustomTitleEnabled,
|
||||
isLiteLog,
|
||||
loadAllProjectsMessageLogs,
|
||||
loadFullLog,
|
||||
loadSameRepoMessageLogs,
|
||||
searchSessionsByCustomTitle,
|
||||
} from '../../utils/sessionStorage.js'
|
||||
import { validateUuid } from '../../utils/uuid.js'
|
||||
|
||||
type ResumeResult =
|
||||
| { resultType: 'sessionNotFound'; arg: string }
|
||||
| { resultType: 'multipleMatches'; arg: string; count: number }
|
||||
|
||||
function resumeHelpMessage(result: ResumeResult): string {
|
||||
switch (result.resultType) {
|
||||
case 'sessionNotFound':
|
||||
return `Session ${chalk.bold(result.arg)} was not found.`;
|
||||
return `Session ${chalk.bold(result.arg)} was not found.`
|
||||
case 'multipleMatches':
|
||||
return `Found ${result.count} sessions matching ${chalk.bold(result.arg)}. Please use /resume to pick a specific session.`;
|
||||
return `Found ${result.count} sessions matching ${chalk.bold(result.arg)}. Please use /resume to pick a specific session.`
|
||||
}
|
||||
}
|
||||
function ResumeError(t0) {
|
||||
const $ = _c(10);
|
||||
const {
|
||||
message,
|
||||
args,
|
||||
onDone
|
||||
} = t0;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== onDone) {
|
||||
t1 = () => {
|
||||
const timer = setTimeout(onDone, 0);
|
||||
return () => clearTimeout(timer);
|
||||
};
|
||||
t2 = [onDone];
|
||||
$[0] = onDone;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
React.useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== args) {
|
||||
t3 = <Text dimColor={true}>{figures.pointer} /resume {args}</Text>;
|
||||
$[3] = args;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== message) {
|
||||
t4 = <MessageResponse><Text>{message}</Text></MessageResponse>;
|
||||
$[5] = message;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
let t5;
|
||||
if ($[7] !== t3 || $[8] !== t4) {
|
||||
t5 = <Box flexDirection="column">{t3}{t4}</Box>;
|
||||
$[7] = t3;
|
||||
$[8] = t4;
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
return t5;
|
||||
|
||||
function ResumeError({
|
||||
message,
|
||||
args,
|
||||
onDone,
|
||||
}: {
|
||||
message: string
|
||||
args: string
|
||||
onDone: () => void
|
||||
}): React.ReactNode {
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(onDone, 0)
|
||||
return () => clearTimeout(timer)
|
||||
}, [onDone])
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor>
|
||||
{figures.pointer} /resume {args}
|
||||
</Text>
|
||||
<MessageResponse>
|
||||
<Text>{message}</Text>
|
||||
</MessageResponse>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function ResumeCommand({
|
||||
onDone,
|
||||
onResume
|
||||
onResume,
|
||||
}: {
|
||||
onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
onResume: (sessionId: UUID, log: LogOption, entrypoint: ResumeEntrypoint) => Promise<void>;
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
onResume: (
|
||||
sessionId: UUID,
|
||||
log: LogOption,
|
||||
entrypoint: ResumeEntrypoint,
|
||||
) => Promise<void>
|
||||
}): React.ReactNode {
|
||||
const [logs, setLogs] = React.useState<LogOption[]>([]);
|
||||
const [worktreePaths, setWorktreePaths] = React.useState<string[]>([]);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [resuming, setResuming] = React.useState(false);
|
||||
const [showAllProjects, setShowAllProjects] = React.useState(false);
|
||||
const {
|
||||
rows
|
||||
} = useTerminalSize();
|
||||
const insideModal = useIsInsideModal();
|
||||
const loadLogs = React.useCallback(async (allProjects: boolean, paths: string[]) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const allLogs = allProjects ? await loadAllProjectsMessageLogs() : await loadSameRepoMessageLogs(paths);
|
||||
const resumable = filterResumableSessions(allLogs, getSessionId());
|
||||
if (resumable.length === 0) {
|
||||
onDone('No conversations found to resume');
|
||||
return;
|
||||
const [logs, setLogs] = React.useState<LogOption[]>([])
|
||||
const [worktreePaths, setWorktreePaths] = React.useState<string[]>([])
|
||||
const [loading, setLoading] = React.useState(true)
|
||||
const [resuming, setResuming] = React.useState(false)
|
||||
const [showAllProjects, setShowAllProjects] = React.useState(false)
|
||||
const { rows } = useTerminalSize()
|
||||
const insideModal = useIsInsideModal()
|
||||
|
||||
const loadLogs = React.useCallback(
|
||||
async (allProjects: boolean, paths: string[]) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const allLogs = allProjects
|
||||
? await loadAllProjectsMessageLogs()
|
||||
: await loadSameRepoMessageLogs(paths)
|
||||
const resumable = filterResumableSessions(allLogs, getSessionId())
|
||||
if (resumable.length === 0) {
|
||||
onDone('No conversations found to resume')
|
||||
return
|
||||
}
|
||||
setLogs(resumable)
|
||||
} catch (_err) {
|
||||
onDone('Failed to load conversations')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
setLogs(resumable);
|
||||
} catch (_err) {
|
||||
onDone('Failed to load conversations');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [onDone]);
|
||||
},
|
||||
[onDone],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
async function init() {
|
||||
const paths_0 = await getWorktreePaths(getOriginalCwd());
|
||||
setWorktreePaths(paths_0);
|
||||
void loadLogs(false, paths_0);
|
||||
const paths = await getWorktreePaths(getOriginalCwd())
|
||||
setWorktreePaths(paths)
|
||||
void loadLogs(false, paths)
|
||||
}
|
||||
void init();
|
||||
}, [loadLogs]);
|
||||
void init()
|
||||
}, [loadLogs])
|
||||
|
||||
const handleToggleAllProjects = React.useCallback(() => {
|
||||
const newValue = !showAllProjects;
|
||||
setShowAllProjects(newValue);
|
||||
void loadLogs(newValue, worktreePaths);
|
||||
}, [showAllProjects, loadLogs, worktreePaths]);
|
||||
const newValue = !showAllProjects
|
||||
setShowAllProjects(newValue)
|
||||
void loadLogs(newValue, worktreePaths)
|
||||
}, [showAllProjects, loadLogs, worktreePaths])
|
||||
|
||||
async function handleSelect(log: LogOption) {
|
||||
const sessionId = validateUuid(getSessionIdFromLog(log));
|
||||
const sessionId = validateUuid(getSessionIdFromLog(log))
|
||||
if (!sessionId) {
|
||||
onDone('Failed to resume conversation');
|
||||
return;
|
||||
onDone('Failed to resume conversation')
|
||||
return
|
||||
}
|
||||
|
||||
// Load full messages for lite logs
|
||||
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log;
|
||||
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log
|
||||
|
||||
// Check if this conversation is from a different directory
|
||||
const crossProjectCheck = checkCrossProjectResume(fullLog, showAllProjects, worktreePaths);
|
||||
const crossProjectCheck = checkCrossProjectResume(
|
||||
fullLog,
|
||||
showAllProjects,
|
||||
worktreePaths,
|
||||
)
|
||||
if (crossProjectCheck.isCrossProject) {
|
||||
if (crossProjectCheck.isSameRepoWorktree) {
|
||||
// Same repo worktree - can resume directly
|
||||
setResuming(true);
|
||||
void onResume(sessionId, fullLog, 'slash_command_picker');
|
||||
return;
|
||||
setResuming(true)
|
||||
void onResume(sessionId, fullLog, 'slash_command_picker')
|
||||
return
|
||||
}
|
||||
|
||||
// Different project - show command instead of resuming
|
||||
const crossCmd = (crossProjectCheck as { isCrossProject: true; isSameRepoWorktree: false; command: string }).command;
|
||||
const raw = await setClipboard(crossCmd);
|
||||
if (raw) process.stdout.write(raw);
|
||||
const raw = await setClipboard(crossProjectCheck.command)
|
||||
if (raw) process.stdout.write(raw)
|
||||
|
||||
// Format the output message
|
||||
const message = ['', 'This conversation is from a different directory.', '', 'To resume, run:', ` ${crossCmd}`, '', '(Command copied to clipboard)', ''].join('\n');
|
||||
onDone(message, {
|
||||
display: 'user'
|
||||
});
|
||||
return;
|
||||
const message = [
|
||||
'',
|
||||
'This conversation is from a different directory.',
|
||||
'',
|
||||
'To resume, run:',
|
||||
` ${crossProjectCheck.command}`,
|
||||
'',
|
||||
'(Command copied to clipboard)',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
onDone(message, { display: 'user' })
|
||||
return
|
||||
}
|
||||
|
||||
// Same directory - proceed with resume
|
||||
setResuming(true);
|
||||
void onResume(sessionId, fullLog, 'slash_command_picker');
|
||||
setResuming(true)
|
||||
void onResume(sessionId, fullLog, 'slash_command_picker')
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
onDone('Resume cancelled', {
|
||||
display: 'system'
|
||||
});
|
||||
onDone('Resume cancelled', { display: 'system' })
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Box>
|
||||
return (
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text> Loading conversations…</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (resuming) {
|
||||
return <Box>
|
||||
return (
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text> Resuming conversation…</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
return <LogSelector logs={logs} maxHeight={insideModal ? Math.floor(rows / 2) : rows - 2} onCancel={handleCancel} onSelect={handleSelect} onLogsChanged={() => loadLogs(showAllProjects, worktreePaths)} showAllProjects={showAllProjects} onToggleAllProjects={handleToggleAllProjects} onAgenticSearch={agenticSessionSearch} />;
|
||||
|
||||
return (
|
||||
<LogSelector
|
||||
logs={logs}
|
||||
maxHeight={insideModal ? Math.floor(rows / 2) : rows - 2}
|
||||
onCancel={handleCancel}
|
||||
onSelect={handleSelect}
|
||||
onLogsChanged={() => loadLogs(showAllProjects, worktreePaths)}
|
||||
showAllProjects={showAllProjects}
|
||||
onToggleAllProjects={handleToggleAllProjects}
|
||||
onAgenticSearch={agenticSessionSearch}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export function filterResumableSessions(logs: LogOption[], currentSessionId: string): LogOption[] {
|
||||
return logs.filter(l => !l.isSidechain && getSessionIdFromLog(l) !== currentSessionId);
|
||||
|
||||
export function filterResumableSessions(
|
||||
logs: LogOption[],
|
||||
currentSessionId: string,
|
||||
): LogOption[] {
|
||||
return logs.filter(
|
||||
l => !l.isSidechain && getSessionIdFromLog(l) !== currentSessionId,
|
||||
)
|
||||
}
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, context, args) => {
|
||||
const onResume = async (sessionId: UUID, log: LogOption, entrypoint: ResumeEntrypoint) => {
|
||||
const onResume = async (
|
||||
sessionId: UUID,
|
||||
log: LogOption,
|
||||
entrypoint: ResumeEntrypoint,
|
||||
) => {
|
||||
try {
|
||||
await context.resume?.(sessionId, log, entrypoint);
|
||||
onDone(undefined, {
|
||||
display: 'skip'
|
||||
});
|
||||
await context.resume?.(sessionId, log, entrypoint)
|
||||
onDone(undefined, { display: 'skip' })
|
||||
} catch (error) {
|
||||
logError(error as Error);
|
||||
onDone(`Failed to resume: ${(error as Error).message}`);
|
||||
logError(error as Error)
|
||||
onDone(`Failed to resume: ${(error as Error).message}`)
|
||||
}
|
||||
};
|
||||
const arg = args?.trim();
|
||||
}
|
||||
|
||||
const arg = args?.trim()
|
||||
|
||||
// No argument provided - show picker
|
||||
if (!arg) {
|
||||
return <ResumeCommand key={Date.now()} onDone={onDone} onResume={onResume} />;
|
||||
return (
|
||||
<ResumeCommand key={Date.now()} onDone={onDone} onResume={onResume} />
|
||||
)
|
||||
}
|
||||
|
||||
// Load logs to search (includes same-repo worktrees)
|
||||
const worktreePaths = await getWorktreePaths(getOriginalCwd());
|
||||
const logs = await loadSameRepoMessageLogs(worktreePaths);
|
||||
const worktreePaths = await getWorktreePaths(getOriginalCwd())
|
||||
const logs = await loadSameRepoMessageLogs(worktreePaths)
|
||||
if (logs.length === 0) {
|
||||
const message = 'No conversations found to resume.';
|
||||
return <ResumeError message={message} args={arg} onDone={() => onDone(message)} />;
|
||||
const message = 'No conversations found to resume.'
|
||||
return (
|
||||
<ResumeError
|
||||
message={message}
|
||||
args={arg}
|
||||
onDone={() => onDone(message)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// First, check if arg is a valid UUID
|
||||
const maybeSessionId = validateUuid(arg);
|
||||
const maybeSessionId = validateUuid(arg)
|
||||
if (maybeSessionId) {
|
||||
const matchingLogs = logs.filter(l => getSessionIdFromLog(l) === maybeSessionId).sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
||||
const matchingLogs = logs
|
||||
.filter(l => getSessionIdFromLog(l) === maybeSessionId)
|
||||
.sort((a, b) => b.modified.getTime() - a.modified.getTime())
|
||||
|
||||
if (matchingLogs.length > 0) {
|
||||
const log = matchingLogs[0]!;
|
||||
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log;
|
||||
void onResume(maybeSessionId, fullLog, 'slash_command_session_id');
|
||||
return null;
|
||||
const log = matchingLogs[0]!
|
||||
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log
|
||||
void onResume(maybeSessionId, fullLog, 'slash_command_session_id')
|
||||
return null
|
||||
}
|
||||
|
||||
// Enriched logs didn't find it — try direct file lookup. This handles
|
||||
// sessions filtered out by enrichLogs (e.g., first message >16KB makes
|
||||
// firstPrompt extraction fail, causing the session to be dropped).
|
||||
const directLog = await getLastSessionLog(maybeSessionId);
|
||||
const directLog = await getLastSessionLog(maybeSessionId)
|
||||
if (directLog) {
|
||||
void onResume(maybeSessionId, directLog, 'slash_command_session_id');
|
||||
return null;
|
||||
void onResume(maybeSessionId, directLog, 'slash_command_session_id')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Next, try exact custom title match (only if feature is enabled)
|
||||
if (isCustomTitleEnabled()) {
|
||||
const titleMatches = await searchSessionsByCustomTitle(arg, {
|
||||
exact: true
|
||||
});
|
||||
exact: true,
|
||||
})
|
||||
if (titleMatches.length === 1) {
|
||||
const log = titleMatches[0]!;
|
||||
const sessionId = getSessionIdFromLog(log);
|
||||
const log = titleMatches[0]!
|
||||
const sessionId = getSessionIdFromLog(log)
|
||||
if (sessionId) {
|
||||
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log;
|
||||
void onResume(sessionId, fullLog, 'slash_command_title');
|
||||
return null;
|
||||
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log
|
||||
void onResume(sessionId, fullLog, 'slash_command_title')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,16 +303,21 @@ export const call: LocalJSXCommandCall = async (onDone, context, args) => {
|
||||
const message = resumeHelpMessage({
|
||||
resultType: 'multipleMatches',
|
||||
arg,
|
||||
count: titleMatches.length
|
||||
});
|
||||
return <ResumeError message={message} args={arg} onDone={() => onDone(message)} />;
|
||||
count: titleMatches.length,
|
||||
})
|
||||
return (
|
||||
<ResumeError
|
||||
message={message}
|
||||
args={arg}
|
||||
onDone={() => onDone(message)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// No match found - show error
|
||||
const message = resumeHelpMessage({
|
||||
resultType: 'sessionNotFound',
|
||||
arg
|
||||
});
|
||||
return <ResumeError message={message} args={arg} onDone={() => onDone(message)} />;
|
||||
};
|
||||
const message = resumeHelpMessage({ resultType: 'sessionNotFound', arg })
|
||||
return (
|
||||
<ResumeError message={message} args={arg} onDone={() => onDone(message)} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,95 +1,71 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { Select } from '../../components/CustomSelect/select.js';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import React, { useCallback, useRef, useState } from 'react'
|
||||
import { Select } from '../../components/CustomSelect/select.js'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
|
||||
type Props = {
|
||||
onProceed: (signal: AbortSignal) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
};
|
||||
export function UltrareviewOverageDialog(t0) {
|
||||
const $ = _c(15);
|
||||
const {
|
||||
onProceed,
|
||||
onCancel
|
||||
} = t0;
|
||||
const [isLaunching, setIsLaunching] = useState(false);
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = new AbortController();
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const abortControllerRef = useRef(t1);
|
||||
let t2;
|
||||
if ($[1] !== onCancel || $[2] !== onProceed) {
|
||||
t2 = value => {
|
||||
if (value === "proceed") {
|
||||
setIsLaunching(true);
|
||||
onProceed(abortControllerRef.current.signal).catch(() => setIsLaunching(false));
|
||||
} else {
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
$[1] = onCancel;
|
||||
$[2] = onProceed;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const handleSelect = t2;
|
||||
let t3;
|
||||
if ($[4] !== onCancel) {
|
||||
t3 = () => {
|
||||
abortControllerRef.current.abort();
|
||||
onCancel();
|
||||
};
|
||||
$[4] = onCancel;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
const handleCancel = t3;
|
||||
let t4;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = [{
|
||||
label: "Proceed with Extra Usage billing",
|
||||
value: "proceed"
|
||||
}, {
|
||||
label: "Cancel",
|
||||
value: "cancel"
|
||||
}];
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
const options = t4;
|
||||
let t5;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Text>Your free ultrareviews for this organization are used. Further reviews bill as Extra Usage (pay-per-use).</Text>;
|
||||
$[7] = t5;
|
||||
} else {
|
||||
t5 = $[7];
|
||||
}
|
||||
let t6;
|
||||
if ($[8] !== handleCancel || $[9] !== handleSelect || $[10] !== isLaunching) {
|
||||
t6 = <Box flexDirection="column" gap={1}>{t5}{isLaunching ? <Text color="background">Launching…</Text> : <Select options={options} onChange={handleSelect} onCancel={handleCancel} />}</Box>;
|
||||
$[8] = handleCancel;
|
||||
$[9] = handleSelect;
|
||||
$[10] = isLaunching;
|
||||
$[11] = t6;
|
||||
} else {
|
||||
t6 = $[11];
|
||||
}
|
||||
let t7;
|
||||
if ($[12] !== handleCancel || $[13] !== t6) {
|
||||
t7 = <Dialog title="Ultrareview billing" onCancel={handleCancel} color="background">{t6}</Dialog>;
|
||||
$[12] = handleCancel;
|
||||
$[13] = t6;
|
||||
$[14] = t7;
|
||||
} else {
|
||||
t7 = $[14];
|
||||
}
|
||||
return t7;
|
||||
onProceed: (signal: AbortSignal) => Promise<void>
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export function UltrareviewOverageDialog({
|
||||
onProceed,
|
||||
onCancel,
|
||||
}: Props): React.ReactNode {
|
||||
const [isLaunching, setIsLaunching] = useState(false)
|
||||
const abortControllerRef = useRef(new AbortController())
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(value: string) => {
|
||||
if (value === 'proceed') {
|
||||
setIsLaunching(true)
|
||||
// If onProceed rejects (e.g. launchRemoteReview throws), onDone is
|
||||
// never called and the dialog stays mounted — restore the Select so
|
||||
// the user can retry or cancel instead of staring at "Launching…".
|
||||
void onProceed(abortControllerRef.current.signal).catch(() =>
|
||||
setIsLaunching(false),
|
||||
)
|
||||
} else {
|
||||
onCancel()
|
||||
}
|
||||
},
|
||||
[onProceed, onCancel],
|
||||
)
|
||||
|
||||
// Escape during launch aborts the in-flight onProceed via signal so the
|
||||
// caller can skip side effects (confirmOverage, onDone) — otherwise a
|
||||
// fire-and-forget launch would keep running and bill despite "cancelled".
|
||||
const handleCancel = useCallback(() => {
|
||||
abortControllerRef.current.abort()
|
||||
onCancel()
|
||||
}, [onCancel])
|
||||
|
||||
const options = [
|
||||
{ label: 'Proceed with Extra Usage billing', value: 'proceed' },
|
||||
{ label: 'Cancel', value: 'cancel' },
|
||||
]
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="Ultrareview billing"
|
||||
onCancel={handleCancel}
|
||||
color="background"
|
||||
>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text>
|
||||
Your free ultrareviews for this organization are used. Further reviews
|
||||
bill as Extra Usage (pay-per-use).
|
||||
</Text>
|
||||
{isLaunching ? (
|
||||
<Text color="background">Launching…</Text>
|
||||
) : (
|
||||
<Select
|
||||
options={options}
|
||||
onChange={handleSelect}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,57 +1,89 @@
|
||||
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.js';
|
||||
import React from 'react';
|
||||
import type { LocalJSXCommandCall, LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { checkOverageGate, confirmOverage, launchRemoteReview } from './reviewRemote.js';
|
||||
import { UltrareviewOverageDialog } from './UltrareviewOverageDialog.js';
|
||||
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.js'
|
||||
import React from 'react'
|
||||
import type {
|
||||
LocalJSXCommandCall,
|
||||
LocalJSXCommandOnDone,
|
||||
} from '../../types/command.js'
|
||||
import {
|
||||
checkOverageGate,
|
||||
confirmOverage,
|
||||
launchRemoteReview,
|
||||
} from './reviewRemote.js'
|
||||
import { UltrareviewOverageDialog } from './UltrareviewOverageDialog.js'
|
||||
|
||||
function contentBlocksToString(blocks: ContentBlockParam[]): string {
|
||||
return blocks.map(b => b.type === 'text' ? b.text : '').filter(Boolean).join('\n');
|
||||
return blocks
|
||||
.map(b => (b.type === 'text' ? b.text : ''))
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
}
|
||||
async function launchAndDone(args: string, context: Parameters<LocalJSXCommandCall>[1], onDone: LocalJSXCommandOnDone, billingNote: string, signal?: AbortSignal): Promise<void> {
|
||||
const result = await launchRemoteReview(args, context, billingNote);
|
||||
|
||||
async function launchAndDone(
|
||||
args: string,
|
||||
context: Parameters<LocalJSXCommandCall>[1],
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
billingNote: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<void> {
|
||||
const result = await launchRemoteReview(args, context, billingNote)
|
||||
// User hit Escape during the ~5s launch — the dialog already showed
|
||||
// "cancelled" and unmounted, so skip onDone (would write to a dead
|
||||
// transcript slot) and let the caller skip confirmOverage.
|
||||
if (signal?.aborted) return;
|
||||
if (signal?.aborted) return
|
||||
if (result) {
|
||||
onDone(contentBlocksToString(result), {
|
||||
shouldQuery: true
|
||||
});
|
||||
onDone(contentBlocksToString(result), { shouldQuery: true })
|
||||
} else {
|
||||
// Precondition failures now return specific ContentBlockParam[] above.
|
||||
// null only reaches here on teleport failure (PR mode) or non-github
|
||||
// repo — both are CCR/repo connectivity issues.
|
||||
onDone('Ultrareview failed to launch the remote session. Check that this is a GitHub repo and try again.', {
|
||||
display: 'system'
|
||||
});
|
||||
onDone(
|
||||
'Ultrareview failed to launch the remote session. Check that this is a GitHub repo and try again.',
|
||||
{ display: 'system' },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, context, args) => {
|
||||
const gate = await checkOverageGate();
|
||||
const gate = await checkOverageGate()
|
||||
|
||||
if (gate.kind === 'not-enabled') {
|
||||
onDone('Free ultrareviews used. Enable Extra Usage at https://claude.ai/settings/billing to continue.', {
|
||||
display: 'system'
|
||||
});
|
||||
return null;
|
||||
onDone(
|
||||
'Free ultrareviews used. Enable Extra Usage at https://claude.ai/settings/billing to continue.',
|
||||
{ display: 'system' },
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
if (gate.kind === 'low-balance') {
|
||||
onDone(`Balance too low to launch ultrareview ($${gate.available.toFixed(2)} available, $10 minimum). Top up at https://claude.ai/settings/billing`, {
|
||||
display: 'system'
|
||||
});
|
||||
return null;
|
||||
onDone(
|
||||
`Balance too low to launch ultrareview ($${gate.available.toFixed(2)} available, $10 minimum). Top up at https://claude.ai/settings/billing`,
|
||||
{ display: 'system' },
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
if (gate.kind === 'needs-confirm') {
|
||||
return <UltrareviewOverageDialog onProceed={async signal => {
|
||||
await launchAndDone(args, context, onDone, ' This review bills as Extra Usage.', signal);
|
||||
// Only persist the confirmation flag after a non-aborted launch —
|
||||
// otherwise Escape-during-launch would leave the flag set and
|
||||
// skip this dialog on the next attempt.
|
||||
if (!signal.aborted) confirmOverage();
|
||||
}} onCancel={() => onDone('Ultrareview cancelled.', {
|
||||
display: 'system'
|
||||
})} />;
|
||||
return (
|
||||
<UltrareviewOverageDialog
|
||||
onProceed={async signal => {
|
||||
await launchAndDone(
|
||||
args,
|
||||
context,
|
||||
onDone,
|
||||
' This review bills as Extra Usage.',
|
||||
signal,
|
||||
)
|
||||
// Only persist the confirmation flag after a non-aborted launch —
|
||||
// otherwise Escape-during-launch would leave the flag set and
|
||||
// skip this dialog on the next attempt.
|
||||
if (!signal.aborted) confirmOverage()
|
||||
}}
|
||||
onCancel={() => onDone('Ultrareview cancelled.', { display: 'system' })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// gate.kind === 'proceed'
|
||||
await launchAndDone(args, context, onDone, gate.billingNote);
|
||||
return null;
|
||||
};
|
||||
await launchAndDone(args, context, onDone, gate.billingNote)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,82 +1,127 @@
|
||||
import { relative } from 'path';
|
||||
import React from 'react';
|
||||
import { getCwdState } from '../../bootstrap/state.js';
|
||||
import { SandboxSettings } from '../../components/sandbox/SandboxSettings.js';
|
||||
import { color } from '../../ink.js';
|
||||
import { getPlatform } from '../../utils/platform.js';
|
||||
import { addToExcludedCommands, SandboxManager } from '../../utils/sandbox/sandbox-adapter.js';
|
||||
import { getSettings_DEPRECATED, getSettingsFilePathForSource } from '../../utils/settings/settings.js';
|
||||
import type { ThemeName } from '../../utils/theme.js';
|
||||
export async function call(onDone: (result?: string) => void, _context: unknown, args?: string): Promise<React.ReactNode | null> {
|
||||
const settings = getSettings_DEPRECATED();
|
||||
const themeName: ThemeName = settings.theme as ThemeName || 'light';
|
||||
const platform = getPlatform();
|
||||
import { relative } from 'path'
|
||||
import React from 'react'
|
||||
import { getCwdState } from '../../bootstrap/state.js'
|
||||
import { SandboxSettings } from '../../components/sandbox/SandboxSettings.js'
|
||||
import { color } from '../../ink.js'
|
||||
import { getPlatform } from '../../utils/platform.js'
|
||||
import {
|
||||
addToExcludedCommands,
|
||||
SandboxManager,
|
||||
} from '../../utils/sandbox/sandbox-adapter.js'
|
||||
import {
|
||||
getSettings_DEPRECATED,
|
||||
getSettingsFilePathForSource,
|
||||
} from '../../utils/settings/settings.js'
|
||||
import type { ThemeName } from '../../utils/theme.js'
|
||||
|
||||
export async function call(
|
||||
onDone: (result?: string) => void,
|
||||
_context: unknown,
|
||||
args?: string,
|
||||
): Promise<React.ReactNode | null> {
|
||||
const settings = getSettings_DEPRECATED()
|
||||
const themeName: ThemeName = (settings.theme as ThemeName) || 'light'
|
||||
|
||||
const platform = getPlatform()
|
||||
|
||||
if (!SandboxManager.isSupportedPlatform()) {
|
||||
// WSL1 users will see this since isSupportedPlatform returns false for WSL1
|
||||
const errorMessage = platform === 'wsl' ? 'Error: Sandboxing requires WSL2. WSL1 is not supported.' : 'Error: Sandboxing is currently only supported on macOS, Linux, and WSL2.';
|
||||
const message = color('error', themeName)(errorMessage);
|
||||
onDone(message);
|
||||
return null;
|
||||
const errorMessage =
|
||||
platform === 'wsl'
|
||||
? 'Error: Sandboxing requires WSL2. WSL1 is not supported.'
|
||||
: 'Error: Sandboxing is currently only supported on macOS, Linux, and WSL2.'
|
||||
const message = color('error', themeName)(errorMessage)
|
||||
onDone(message)
|
||||
return null
|
||||
}
|
||||
|
||||
// Check dependencies - get structured result with errors/warnings
|
||||
const depCheck = SandboxManager.checkDependencies();
|
||||
const depCheck = SandboxManager.checkDependencies()
|
||||
|
||||
// Check if platform is in enabledPlatforms list (undocumented enterprise setting)
|
||||
if (!SandboxManager.isPlatformInEnabledList()) {
|
||||
const message = color('error', themeName)(`Error: Sandboxing is disabled for this platform (${platform}) via the enabledPlatforms setting.`);
|
||||
onDone(message);
|
||||
return null;
|
||||
const message = color(
|
||||
'error',
|
||||
themeName,
|
||||
)(
|
||||
`Error: Sandboxing is disabled for this platform (${platform}) via the enabledPlatforms setting.`,
|
||||
)
|
||||
onDone(message)
|
||||
return null
|
||||
}
|
||||
|
||||
// Check if sandbox settings are locked by higher-priority settings
|
||||
if (SandboxManager.areSandboxSettingsLockedByPolicy()) {
|
||||
const message = color('error', themeName)('Error: Sandbox settings are overridden by a higher-priority configuration and cannot be changed locally.');
|
||||
onDone(message);
|
||||
return null;
|
||||
const message = color(
|
||||
'error',
|
||||
themeName,
|
||||
)(
|
||||
'Error: Sandbox settings are overridden by a higher-priority configuration and cannot be changed locally.',
|
||||
)
|
||||
onDone(message)
|
||||
return null
|
||||
}
|
||||
|
||||
// Parse the arguments
|
||||
const trimmedArgs = args?.trim() || '';
|
||||
const trimmedArgs = args?.trim() || ''
|
||||
|
||||
// If no args, show the interactive menu
|
||||
if (!trimmedArgs) {
|
||||
return <SandboxSettings onComplete={onDone} depCheck={depCheck} />;
|
||||
return <SandboxSettings onComplete={onDone} depCheck={depCheck} />
|
||||
}
|
||||
|
||||
// Handle subcommands
|
||||
if (trimmedArgs) {
|
||||
const parts = trimmedArgs.split(' ');
|
||||
const subcommand = parts[0];
|
||||
const parts = trimmedArgs.split(' ')
|
||||
const subcommand = parts[0]
|
||||
|
||||
if (subcommand === 'exclude') {
|
||||
// Handle exclude subcommand
|
||||
const commandPattern = trimmedArgs.slice('exclude '.length).trim();
|
||||
const commandPattern = trimmedArgs.slice('exclude '.length).trim()
|
||||
|
||||
if (!commandPattern) {
|
||||
const message = color('error', themeName)('Error: Please provide a command pattern to exclude (e.g., /sandbox exclude "npm run test:*")');
|
||||
onDone(message);
|
||||
return null;
|
||||
const message = color(
|
||||
'error',
|
||||
themeName,
|
||||
)(
|
||||
'Error: Please provide a command pattern to exclude (e.g., /sandbox exclude "npm run test:*")',
|
||||
)
|
||||
onDone(message)
|
||||
return null
|
||||
}
|
||||
|
||||
// Remove quotes if present
|
||||
const cleanPattern = commandPattern.replace(/^["']|["']$/g, '');
|
||||
const cleanPattern = commandPattern.replace(/^["']|["']$/g, '')
|
||||
|
||||
// Add to excludedCommands
|
||||
addToExcludedCommands(cleanPattern);
|
||||
addToExcludedCommands(cleanPattern)
|
||||
|
||||
// Get the local settings path and make it relative to cwd
|
||||
const localSettingsPath = getSettingsFilePathForSource('localSettings');
|
||||
const relativePath = localSettingsPath ? relative(getCwdState(), localSettingsPath) : '.claude/settings.local.json';
|
||||
const message = color('success', themeName)(`Added "${cleanPattern}" to excluded commands in ${relativePath}`);
|
||||
onDone(message);
|
||||
return null;
|
||||
const localSettingsPath = getSettingsFilePathForSource('localSettings')
|
||||
const relativePath = localSettingsPath
|
||||
? relative(getCwdState(), localSettingsPath)
|
||||
: '.claude/settings.local.json'
|
||||
|
||||
const message = color(
|
||||
'success',
|
||||
themeName,
|
||||
)(`Added "${cleanPattern}" to excluded commands in ${relativePath}`)
|
||||
|
||||
onDone(message)
|
||||
return null
|
||||
} else {
|
||||
// Unknown subcommand
|
||||
const message = color('error', themeName)(`Error: Unknown subcommand "${subcommand}". Available subcommand: exclude`);
|
||||
onDone(message);
|
||||
return null;
|
||||
const message = color(
|
||||
'error',
|
||||
themeName,
|
||||
)(
|
||||
`Error: Unknown subcommand "${subcommand}". Available subcommand: exclude`,
|
||||
)
|
||||
onDone(message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Should never reach here since we handle all cases above
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,139 +1,83 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { toString as qrToString } from 'qrcode';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Pane } from '../../components/design-system/Pane.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||||
import { useAppState } from '../../state/AppState.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import { logForDebugging } from '../../utils/debug.js';
|
||||
import { toString as qrToString } from 'qrcode'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Pane } from '../../components/design-system/Pane.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||
import { useAppState } from '../../state/AppState.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
|
||||
type Props = {
|
||||
onDone: () => void;
|
||||
};
|
||||
function SessionInfo(t0) {
|
||||
const $ = _c(19);
|
||||
const {
|
||||
onDone
|
||||
} = t0;
|
||||
const remoteSessionUrl = useAppState(_temp);
|
||||
const [qrCode, setQrCode] = useState("");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== remoteSessionUrl) {
|
||||
t1 = () => {
|
||||
if (!remoteSessionUrl) {
|
||||
return;
|
||||
}
|
||||
const url = remoteSessionUrl;
|
||||
const generateQRCode = async function generateQRCode() {
|
||||
const qr = await qrToString(url, {
|
||||
type: "utf8",
|
||||
errorCorrectionLevel: "L"
|
||||
});
|
||||
setQrCode(qr);
|
||||
};
|
||||
generateQRCode().catch(_temp2);
|
||||
};
|
||||
t2 = [remoteSessionUrl];
|
||||
$[0] = remoteSessionUrl;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
useKeybinding("confirm:no", onDone, t3);
|
||||
onDone: () => void
|
||||
}
|
||||
|
||||
function SessionInfo({ onDone }: Props): React.ReactNode {
|
||||
const remoteSessionUrl = useAppState(s => s.remoteSessionUrl)
|
||||
const [qrCode, setQrCode] = useState<string>('')
|
||||
|
||||
// Generate QR code when URL is available
|
||||
useEffect(() => {
|
||||
if (!remoteSessionUrl) return
|
||||
|
||||
const url = remoteSessionUrl
|
||||
async function generateQRCode(): Promise<void> {
|
||||
const qr = await qrToString(url, {
|
||||
type: 'utf8',
|
||||
errorCorrectionLevel: 'L',
|
||||
})
|
||||
setQrCode(qr)
|
||||
}
|
||||
// Intentionally silent fail - URL is still shown so QR is non-critical
|
||||
generateQRCode().catch(e => {
|
||||
logForDebugging('QR code generation failed', e)
|
||||
})
|
||||
}, [remoteSessionUrl])
|
||||
|
||||
// Handle ESC to dismiss
|
||||
useKeybinding('confirm:no', onDone, { context: 'Confirmation' })
|
||||
|
||||
// Not in remote mode
|
||||
if (!remoteSessionUrl) {
|
||||
let t4;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Pane><Text color="warning">Not in remote mode. Start with `claude --remote` to use this command.</Text><Text dimColor={true}>(press esc to close)</Text></Pane>;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
return t4;
|
||||
return (
|
||||
<Pane>
|
||||
<Text color="warning">
|
||||
Not in remote mode. Start with `claude --remote` to use this command.
|
||||
</Text>
|
||||
<Text dimColor>(press esc to close)</Text>
|
||||
</Pane>
|
||||
)
|
||||
}
|
||||
let T0;
|
||||
let t4;
|
||||
let t5;
|
||||
if ($[5] !== qrCode) {
|
||||
const lines = qrCode.split("\n").filter(_temp3);
|
||||
const isLoading = lines.length === 0;
|
||||
T0 = Pane;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Box marginBottom={1}><Text bold={true}>Remote session</Text></Box>;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
t5 = isLoading ? <Text dimColor={true}>Generating QR code…</Text> : lines.map(_temp4);
|
||||
$[5] = qrCode;
|
||||
$[6] = T0;
|
||||
$[7] = t4;
|
||||
$[8] = t5;
|
||||
} else {
|
||||
T0 = $[6];
|
||||
t4 = $[7];
|
||||
t5 = $[8];
|
||||
}
|
||||
let t6;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = <Text dimColor={true}>Open in browser: </Text>;
|
||||
$[10] = t6;
|
||||
} else {
|
||||
t6 = $[10];
|
||||
}
|
||||
let t7;
|
||||
if ($[11] !== remoteSessionUrl) {
|
||||
t7 = <Box marginTop={1}>{t6}<Text color="ide">{remoteSessionUrl}</Text></Box>;
|
||||
$[11] = remoteSessionUrl;
|
||||
$[12] = t7;
|
||||
} else {
|
||||
t7 = $[12];
|
||||
}
|
||||
let t8;
|
||||
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = <Box marginTop={1}><Text dimColor={true}>(press esc to close)</Text></Box>;
|
||||
$[13] = t8;
|
||||
} else {
|
||||
t8 = $[13];
|
||||
}
|
||||
let t9;
|
||||
if ($[14] !== T0 || $[15] !== t4 || $[16] !== t5 || $[17] !== t7) {
|
||||
t9 = <T0>{t4}{t5}{t7}{t8}</T0>;
|
||||
$[14] = T0;
|
||||
$[15] = t4;
|
||||
$[16] = t5;
|
||||
$[17] = t7;
|
||||
$[18] = t9;
|
||||
} else {
|
||||
t9 = $[18];
|
||||
}
|
||||
return t9;
|
||||
}
|
||||
function _temp4(line_0, i) {
|
||||
return <Text key={i}>{line_0}</Text>;
|
||||
}
|
||||
function _temp3(line) {
|
||||
return line.length > 0;
|
||||
}
|
||||
function _temp2(e) {
|
||||
logForDebugging("QR code generation failed", e);
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.remoteSessionUrl;
|
||||
|
||||
const lines = qrCode.split('\n').filter(line => line.length > 0)
|
||||
const isLoading = lines.length === 0
|
||||
|
||||
return (
|
||||
<Pane>
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>Remote session</Text>
|
||||
</Box>
|
||||
|
||||
{/* QR Code - silently fails if generation errors, URL is still shown */}
|
||||
{isLoading ? (
|
||||
<Text dimColor>Generating QR code…</Text>
|
||||
) : (
|
||||
lines.map((line, i) => <Text key={i}>{line}</Text>)
|
||||
)}
|
||||
|
||||
{/* URL */}
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Open in browser: </Text>
|
||||
<Text color="ide">{remoteSessionUrl}</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>(press esc to close)</Text>
|
||||
</Box>
|
||||
</Pane>
|
||||
)
|
||||
}
|
||||
|
||||
export const call: LocalJSXCommandCall = async onDone => {
|
||||
return <SessionInfo onDone={onDone} />;
|
||||
};
|
||||
return <SessionInfo onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import type { LocalJSXCommandContext } from '../../commands.js';
|
||||
import { SkillsMenu } from '../../components/skills/SkillsMenu.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext): Promise<React.ReactNode> {
|
||||
return <SkillsMenu onExit={onDone} commands={context.options.commands} />;
|
||||
import * as React from 'react'
|
||||
import type { LocalJSXCommandContext } from '../../commands.js'
|
||||
import { SkillsMenu } from '../../components/skills/SkillsMenu.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
): Promise<React.ReactNode> {
|
||||
return <SkillsMenu onExit={onDone} commands={context.options.commands} />
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { Stats } from '../../components/Stats.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import * as React from 'react'
|
||||
import { Stats } from '../../components/Stats.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
|
||||
export const call: LocalJSXCommandCall = async onDone => {
|
||||
return <Stats onClose={onDone} />;
|
||||
};
|
||||
return <Stats onClose={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import type { LocalJSXCommandContext } from '../../commands.js';
|
||||
import { Settings } from '../../components/Settings/Settings.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext): Promise<React.ReactNode> {
|
||||
return <Settings onClose={onDone} context={context} defaultTab="Status" />;
|
||||
import * as React from 'react'
|
||||
import type { LocalJSXCommandContext } from '../../commands.js'
|
||||
import { Settings } from '../../components/Settings/Settings.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
): Promise<React.ReactNode> {
|
||||
return <Settings onClose={onDone} context={context} defaultTab="Status" />
|
||||
}
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import type { Command } from '../commands.js';
|
||||
import { AGENT_TOOL_NAME } from '../tools/AgentTool/constants.js';
|
||||
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import type { Command } from '../commands.js'
|
||||
import { AGENT_TOOL_NAME } from '../tools/AgentTool/constants.js'
|
||||
|
||||
const statusline = {
|
||||
type: 'prompt',
|
||||
description: "Set up Claude Code's status line UI",
|
||||
contentLength: 0,
|
||||
// Dynamic content
|
||||
contentLength: 0, // Dynamic content
|
||||
aliases: [],
|
||||
name: 'statusline',
|
||||
progressMessage: 'setting up statusLine',
|
||||
allowedTools: [AGENT_TOOL_NAME, 'Read(~/**)', 'Edit(~/.claude/settings.json)'],
|
||||
allowedTools: [
|
||||
AGENT_TOOL_NAME,
|
||||
'Read(~/**)',
|
||||
'Edit(~/.claude/settings.json)',
|
||||
],
|
||||
source: 'builtin',
|
||||
disableNonInteractive: true,
|
||||
async getPromptForCommand(args): Promise<ContentBlockParam[]> {
|
||||
const prompt = args.trim() || 'Configure my statusLine from my shell PS1 configuration';
|
||||
return [{
|
||||
type: 'text',
|
||||
text: `Create an ${AGENT_TOOL_NAME} with subagent_type "statusline-setup" and the prompt "${prompt}"`
|
||||
}];
|
||||
}
|
||||
} satisfies Command;
|
||||
export default statusline;
|
||||
const prompt =
|
||||
args.trim() || 'Configure my statusLine from my shell PS1 configuration'
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Create an ${AGENT_TOOL_NAME} with subagent_type "statusline-setup" and the prompt "${prompt}"`,
|
||||
},
|
||||
]
|
||||
},
|
||||
} satisfies Command
|
||||
|
||||
export default statusline
|
||||
|
||||
@@ -1,214 +1,167 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import chalk from 'chalk';
|
||||
import type { UUID } from 'crypto';
|
||||
import * as React from 'react';
|
||||
import { getSessionId } from '../../bootstrap/state.js';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import { Select } from '../../components/CustomSelect/select.js';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { logEvent } from '../../services/analytics/index.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { recursivelySanitizeUnicode } from '../../utils/sanitization.js';
|
||||
import { getCurrentSessionTag, getTranscriptPath, saveTag } from '../../utils/sessionStorage.js';
|
||||
function ConfirmRemoveTag(t0) {
|
||||
const $ = _c(11);
|
||||
const {
|
||||
tagName,
|
||||
onConfirm,
|
||||
onCancel
|
||||
} = t0;
|
||||
const t1 = `Current tag: #${tagName}`;
|
||||
let t2;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Text>This will remove the tag from the current session.</Text>;
|
||||
$[0] = t2;
|
||||
} else {
|
||||
t2 = $[0];
|
||||
}
|
||||
let t3;
|
||||
if ($[1] !== onCancel || $[2] !== onConfirm) {
|
||||
t3 = value => value === "yes" ? onConfirm() : onCancel();
|
||||
$[1] = onCancel;
|
||||
$[2] = onConfirm;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
let t4;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = [{
|
||||
label: "Yes, remove tag",
|
||||
value: "yes"
|
||||
}, {
|
||||
label: "No, keep tag",
|
||||
value: "no"
|
||||
}];
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
let t5;
|
||||
if ($[5] !== t3) {
|
||||
t5 = <Box flexDirection="column" gap={1}>{t2}<Select onChange={t3} options={t4} /></Box>;
|
||||
$[5] = t3;
|
||||
$[6] = t5;
|
||||
} else {
|
||||
t5 = $[6];
|
||||
}
|
||||
let t6;
|
||||
if ($[7] !== onCancel || $[8] !== t1 || $[9] !== t5) {
|
||||
t6 = <Dialog title="Remove tag?" subtitle={t1} onCancel={onCancel} color="warning">{t5}</Dialog>;
|
||||
$[7] = onCancel;
|
||||
$[8] = t1;
|
||||
$[9] = t5;
|
||||
$[10] = t6;
|
||||
} else {
|
||||
t6 = $[10];
|
||||
}
|
||||
return t6;
|
||||
import chalk from 'chalk'
|
||||
import type { UUID } from 'crypto'
|
||||
import * as React from 'react'
|
||||
import { getSessionId } from '../../bootstrap/state.js'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import { Select } from '../../components/CustomSelect/select.js'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { logEvent } from '../../services/analytics/index.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import { recursivelySanitizeUnicode } from '../../utils/sanitization.js'
|
||||
import {
|
||||
getCurrentSessionTag,
|
||||
getTranscriptPath,
|
||||
saveTag,
|
||||
} from '../../utils/sessionStorage.js'
|
||||
|
||||
function ConfirmRemoveTag({
|
||||
tagName,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: {
|
||||
tagName: string
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
}): React.ReactNode {
|
||||
return (
|
||||
<Dialog
|
||||
title="Remove tag?"
|
||||
subtitle={`Current tag: #${tagName}`}
|
||||
onCancel={onCancel}
|
||||
color="warning"
|
||||
>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text>This will remove the tag from the current session.</Text>
|
||||
<Select<'yes' | 'no'>
|
||||
onChange={value => (value === 'yes' ? onConfirm() : onCancel())}
|
||||
options={[
|
||||
{ label: 'Yes, remove tag', value: 'yes' },
|
||||
{ label: 'No, keep tag', value: 'no' },
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
function ToggleTagAndClose(t0) {
|
||||
const $ = _c(17);
|
||||
const {
|
||||
tagName,
|
||||
onDone
|
||||
} = t0;
|
||||
const [showConfirm, setShowConfirm] = React.useState(false);
|
||||
const [sessionId, setSessionId] = React.useState(null);
|
||||
let t1;
|
||||
if ($[0] !== tagName) {
|
||||
t1 = recursivelySanitizeUnicode(tagName).trim();
|
||||
$[0] = tagName;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const normalizedTag = t1;
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[2] !== normalizedTag || $[3] !== onDone) {
|
||||
t2 = () => {
|
||||
const id = getSessionId() as UUID;
|
||||
if (!id) {
|
||||
onDone("No active session to tag", {
|
||||
display: "system"
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!normalizedTag) {
|
||||
onDone("Tag name cannot be empty", {
|
||||
display: "system"
|
||||
});
|
||||
return;
|
||||
}
|
||||
setSessionId(id);
|
||||
const currentTag = getCurrentSessionTag(id);
|
||||
if (currentTag === normalizedTag) {
|
||||
logEvent("tengu_tag_command_remove_prompt", {});
|
||||
setShowConfirm(true);
|
||||
} else {
|
||||
const isReplacing = !!currentTag;
|
||||
logEvent("tengu_tag_command_add", {
|
||||
is_replacing: isReplacing
|
||||
});
|
||||
(async () => {
|
||||
const fullPath = getTranscriptPath();
|
||||
await saveTag(id, normalizedTag, fullPath);
|
||||
onDone(`Tagged session with ${chalk.cyan(`#${normalizedTag}`)}`, {
|
||||
display: "system"
|
||||
});
|
||||
})();
|
||||
}
|
||||
};
|
||||
t3 = [normalizedTag, onDone];
|
||||
$[2] = normalizedTag;
|
||||
$[3] = onDone;
|
||||
$[4] = t2;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
t3 = $[5];
|
||||
}
|
||||
React.useEffect(t2, t3);
|
||||
|
||||
function ToggleTagAndClose({
|
||||
tagName,
|
||||
onDone,
|
||||
}: {
|
||||
tagName: string
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
}): React.ReactNode {
|
||||
const [showConfirm, setShowConfirm] = React.useState(false)
|
||||
const [sessionId, setSessionId] = React.useState<UUID | null>(null)
|
||||
// Sanitize unicode to prevent hidden character attacks and normalize
|
||||
const normalizedTag = recursivelySanitizeUnicode(tagName).trim()
|
||||
|
||||
React.useEffect(() => {
|
||||
const id = getSessionId() as UUID
|
||||
|
||||
if (!id) {
|
||||
onDone('No active session to tag', { display: 'system' })
|
||||
return
|
||||
}
|
||||
|
||||
if (!normalizedTag) {
|
||||
onDone('Tag name cannot be empty', { display: 'system' })
|
||||
return
|
||||
}
|
||||
|
||||
setSessionId(id)
|
||||
const currentTag = getCurrentSessionTag(id)
|
||||
|
||||
// If same tag exists, show confirmation dialog
|
||||
if (currentTag === normalizedTag) {
|
||||
logEvent('tengu_tag_command_remove_prompt', {})
|
||||
setShowConfirm(true)
|
||||
} else {
|
||||
// Add the new tag directly
|
||||
const isReplacing = !!currentTag
|
||||
logEvent('tengu_tag_command_add', { is_replacing: isReplacing })
|
||||
void (async () => {
|
||||
const fullPath = getTranscriptPath()
|
||||
await saveTag(id, normalizedTag, fullPath)
|
||||
onDone(`Tagged session with ${chalk.cyan(`#${normalizedTag}`)}`, {
|
||||
display: 'system',
|
||||
})
|
||||
})()
|
||||
}
|
||||
}, [normalizedTag, onDone])
|
||||
|
||||
if (showConfirm && sessionId) {
|
||||
let t4;
|
||||
if ($[6] !== normalizedTag || $[7] !== onDone || $[8] !== sessionId) {
|
||||
t4 = async () => {
|
||||
logEvent("tengu_tag_command_remove_confirmed", {});
|
||||
const fullPath_0 = getTranscriptPath();
|
||||
await saveTag(sessionId, "", fullPath_0);
|
||||
onDone(`Removed tag ${chalk.cyan(`#${normalizedTag}`)}`, {
|
||||
display: "system"
|
||||
});
|
||||
};
|
||||
$[6] = normalizedTag;
|
||||
$[7] = onDone;
|
||||
$[8] = sessionId;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
let t5;
|
||||
if ($[10] !== normalizedTag || $[11] !== onDone) {
|
||||
t5 = () => {
|
||||
logEvent("tengu_tag_command_remove_cancelled", {});
|
||||
onDone(`Kept tag ${chalk.cyan(`#${normalizedTag}`)}`, {
|
||||
display: "system"
|
||||
});
|
||||
};
|
||||
$[10] = normalizedTag;
|
||||
$[11] = onDone;
|
||||
$[12] = t5;
|
||||
} else {
|
||||
t5 = $[12];
|
||||
}
|
||||
let t6;
|
||||
if ($[13] !== normalizedTag || $[14] !== t4 || $[15] !== t5) {
|
||||
t6 = <ConfirmRemoveTag tagName={normalizedTag} onConfirm={t4} onCancel={t5} />;
|
||||
$[13] = normalizedTag;
|
||||
$[14] = t4;
|
||||
$[15] = t5;
|
||||
$[16] = t6;
|
||||
} else {
|
||||
t6 = $[16];
|
||||
}
|
||||
return t6;
|
||||
return (
|
||||
<ConfirmRemoveTag
|
||||
tagName={normalizedTag}
|
||||
onConfirm={async () => {
|
||||
logEvent('tengu_tag_command_remove_confirmed', {})
|
||||
const fullPath = getTranscriptPath()
|
||||
await saveTag(sessionId, '', fullPath)
|
||||
onDone(`Removed tag ${chalk.cyan(`#${normalizedTag}`)}`, {
|
||||
display: 'system',
|
||||
})
|
||||
}}
|
||||
onCancel={() => {
|
||||
logEvent('tengu_tag_command_remove_cancelled', {})
|
||||
onDone(`Kept tag ${chalk.cyan(`#${normalizedTag}`)}`, {
|
||||
display: 'system',
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return null;
|
||||
|
||||
return null
|
||||
}
|
||||
function ShowHelp(t0) {
|
||||
const $ = _c(3);
|
||||
const {
|
||||
onDone
|
||||
} = t0;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== onDone) {
|
||||
t1 = () => {
|
||||
onDone("Usage: /tag <tag-name>\n\nToggle a searchable tag on the current session.\nRun the same command again to remove the tag.\nTags are displayed after the branch name in /resume and can be searched with /.\n\nExamples:\n /tag bugfix # Add tag\n /tag bugfix # Remove tag (toggle)\n /tag feature-auth\n /tag wip", {
|
||||
display: "system"
|
||||
});
|
||||
};
|
||||
t2 = [onDone];
|
||||
$[0] = onDone;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
React.useEffect(t1, t2);
|
||||
return null;
|
||||
|
||||
function ShowHelp({
|
||||
onDone,
|
||||
}: {
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
}): React.ReactNode {
|
||||
React.useEffect(() => {
|
||||
onDone(
|
||||
`Usage: /tag <tag-name>
|
||||
|
||||
Toggle a searchable tag on the current session.
|
||||
Run the same command again to remove the tag.
|
||||
Tags are displayed after the branch name in /resume and can be searched with /.
|
||||
|
||||
Examples:
|
||||
/tag bugfix # Add tag
|
||||
/tag bugfix # Remove tag (toggle)
|
||||
/tag feature-auth
|
||||
/tag wip`,
|
||||
{ display: 'system' },
|
||||
)
|
||||
}, [onDone])
|
||||
|
||||
return null
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, _context: unknown, args?: string): Promise<React.ReactNode> {
|
||||
args = args?.trim() || '';
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
_context: unknown,
|
||||
args?: string,
|
||||
): Promise<React.ReactNode> {
|
||||
args = args?.trim() || ''
|
||||
|
||||
if (COMMON_INFO_ARGS.includes(args) || COMMON_HELP_ARGS.includes(args)) {
|
||||
return <ShowHelp onDone={onDone} />;
|
||||
return <ShowHelp onDone={onDone} />
|
||||
}
|
||||
|
||||
if (!args) {
|
||||
return <ShowHelp onDone={onDone} />;
|
||||
return <ShowHelp onDone={onDone} />
|
||||
}
|
||||
return <ToggleTagAndClose tagName={args} onDone={onDone} />;
|
||||
|
||||
return <ToggleTagAndClose tagName={args} onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import type { LocalJSXCommandContext } from '../../commands.js';
|
||||
import { BackgroundTasksDialog } from '../../components/tasks/BackgroundTasksDialog.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext): Promise<React.ReactNode> {
|
||||
return <BackgroundTasksDialog toolUseContext={context} onDone={onDone} />;
|
||||
import * as React from 'react'
|
||||
import type { LocalJSXCommandContext } from '../../commands.js'
|
||||
import { BackgroundTasksDialog } from '../../components/tasks/BackgroundTasksDialog.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
): Promise<React.ReactNode> {
|
||||
return <BackgroundTasksDialog toolUseContext={context} onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
import chalk from 'chalk';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { copyFile, mkdir, readFile, writeFile } from 'fs/promises';
|
||||
import { homedir, platform } from 'os';
|
||||
import { dirname, join } from 'path';
|
||||
import type { ThemeName } from 'src/utils/theme.js';
|
||||
import { pathToFileURL } from 'url';
|
||||
import { supportsHyperlinks } from '../../ink/supports-hyperlinks.js';
|
||||
import { color } from '../../ink.js';
|
||||
import { maybeMarkProjectOnboardingComplete } from '../../projectOnboardingState.js';
|
||||
import type { ToolUseContext } from '../../Tool.js';
|
||||
import type { LocalJSXCommandContext, LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { backupTerminalPreferences, checkAndRestoreTerminalBackup, getTerminalPlistPath, markTerminalSetupComplete } from '../../utils/appleTerminalBackup.js';
|
||||
import { setupShellCompletion } from '../../utils/completionCache.js';
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
|
||||
import { env } from '../../utils/env.js';
|
||||
import { isFsInaccessible } from '../../utils/errors.js';
|
||||
import { execFileNoThrow } from '../../utils/execFileNoThrow.js';
|
||||
import { addItemToJSONCArray, safeParseJSONC } from '../../utils/json.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import { getPlatform } from '../../utils/platform.js';
|
||||
import { jsonParse, jsonStringify } from '../../utils/slowOperations.js';
|
||||
const EOL = '\n';
|
||||
import chalk from 'chalk'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { copyFile, mkdir, readFile, writeFile } from 'fs/promises'
|
||||
import { homedir, platform } from 'os'
|
||||
import { dirname, join } from 'path'
|
||||
import type { ThemeName } from 'src/utils/theme.js'
|
||||
import { pathToFileURL } from 'url'
|
||||
import { supportsHyperlinks } from '../../ink/supports-hyperlinks.js'
|
||||
import { color } from '../../ink.js'
|
||||
import { maybeMarkProjectOnboardingComplete } from '../../projectOnboardingState.js'
|
||||
import type { ToolUseContext } from '../../Tool.js'
|
||||
import type {
|
||||
LocalJSXCommandContext,
|
||||
LocalJSXCommandOnDone,
|
||||
} from '../../types/command.js'
|
||||
import {
|
||||
backupTerminalPreferences,
|
||||
checkAndRestoreTerminalBackup,
|
||||
getTerminalPlistPath,
|
||||
markTerminalSetupComplete,
|
||||
} from '../../utils/appleTerminalBackup.js'
|
||||
import { setupShellCompletion } from '../../utils/completionCache.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||
import { env } from '../../utils/env.js'
|
||||
import { isFsInaccessible } from '../../utils/errors.js'
|
||||
import { execFileNoThrow } from '../../utils/execFileNoThrow.js'
|
||||
import { addItemToJSONCArray, safeParseJSONC } from '../../utils/json.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import { getPlatform } from '../../utils/platform.js'
|
||||
import { jsonParse, jsonStringify } from '../../utils/slowOperations.js'
|
||||
|
||||
const EOL = '\n'
|
||||
|
||||
// Terminals that natively support CSI u / Kitty keyboard protocol
|
||||
const NATIVE_CSIU_TERMINALS: Record<string, string> = {
|
||||
@@ -28,8 +37,8 @@ const NATIVE_CSIU_TERMINALS: Record<string, string> = {
|
||||
kitty: 'Kitty',
|
||||
'iTerm.app': 'iTerm2',
|
||||
WezTerm: 'WezTerm',
|
||||
WarpTerminal: 'Warp'
|
||||
};
|
||||
WarpTerminal: 'Warp',
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if we're running in a VSCode Remote SSH session.
|
||||
@@ -37,18 +46,26 @@ const NATIVE_CSIU_TERMINALS: Record<string, string> = {
|
||||
* not the remote server where Claude is running.
|
||||
*/
|
||||
function isVSCodeRemoteSSH(): boolean {
|
||||
const askpassMain = process.env.VSCODE_GIT_ASKPASS_MAIN ?? '';
|
||||
const path = process.env.PATH ?? '';
|
||||
const askpassMain = process.env.VSCODE_GIT_ASKPASS_MAIN ?? ''
|
||||
const path = process.env.PATH ?? ''
|
||||
|
||||
// Check both env vars - VSCODE_GIT_ASKPASS_MAIN is more reliable when git extension
|
||||
// is active, and PATH is a fallback. Omit path separator for Windows compatibility.
|
||||
return askpassMain.includes('.vscode-server') || askpassMain.includes('.cursor-server') || askpassMain.includes('.windsurf-server') || path.includes('.vscode-server') || path.includes('.cursor-server') || path.includes('.windsurf-server');
|
||||
return (
|
||||
askpassMain.includes('.vscode-server') ||
|
||||
askpassMain.includes('.cursor-server') ||
|
||||
askpassMain.includes('.windsurf-server') ||
|
||||
path.includes('.vscode-server') ||
|
||||
path.includes('.cursor-server') ||
|
||||
path.includes('.windsurf-server')
|
||||
)
|
||||
}
|
||||
|
||||
export function getNativeCSIuTerminalDisplayName(): string | null {
|
||||
if (!env.terminal || !(env.terminal in NATIVE_CSIU_TERMINALS)) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
return NATIVE_CSIU_TERMINALS[env.terminal] ?? null;
|
||||
return NATIVE_CSIU_TERMINALS[env.terminal] ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,101 +81,120 @@ export function getNativeCSIuTerminalDisplayName(): string | null {
|
||||
*/
|
||||
function formatPathLink(filePath: string): string {
|
||||
if (!supportsHyperlinks()) {
|
||||
return filePath;
|
||||
return filePath
|
||||
}
|
||||
const fileUrl = pathToFileURL(filePath).href;
|
||||
const fileUrl = pathToFileURL(filePath).href
|
||||
// OSC 8 hyperlink: \e]8;;URL\a TEXT \e]8;;\a
|
||||
return `\x1b]8;;${fileUrl}\x07${filePath}\x1b]8;;\x07`;
|
||||
return `\x1b]8;;${fileUrl}\x07${filePath}\x1b]8;;\x07`
|
||||
}
|
||||
|
||||
export function shouldOfferTerminalSetup(): boolean {
|
||||
// iTerm2, WezTerm, Ghostty, Kitty, and Warp natively support CSI u / Kitty
|
||||
// keyboard protocol, which Claude Code already parses. No setup needed for
|
||||
// these terminals.
|
||||
return platform() === 'darwin' && env.terminal === 'Apple_Terminal' || env.terminal === 'vscode' || env.terminal === 'cursor' || env.terminal === 'windsurf' || env.terminal === 'alacritty' || env.terminal === 'zed';
|
||||
return (
|
||||
(platform() === 'darwin' && env.terminal === 'Apple_Terminal') ||
|
||||
env.terminal === 'vscode' ||
|
||||
env.terminal === 'cursor' ||
|
||||
env.terminal === 'windsurf' ||
|
||||
env.terminal === 'alacritty' ||
|
||||
env.terminal === 'zed'
|
||||
)
|
||||
}
|
||||
|
||||
export async function setupTerminal(theme: ThemeName): Promise<string> {
|
||||
let result = '';
|
||||
let result = ''
|
||||
|
||||
switch (env.terminal) {
|
||||
case 'Apple_Terminal':
|
||||
result = await enableOptionAsMetaForTerminal(theme);
|
||||
break;
|
||||
result = await enableOptionAsMetaForTerminal(theme)
|
||||
break
|
||||
case 'vscode':
|
||||
result = await installBindingsForVSCodeTerminal('VSCode', theme);
|
||||
break;
|
||||
result = await installBindingsForVSCodeTerminal('VSCode', theme)
|
||||
break
|
||||
case 'cursor':
|
||||
result = await installBindingsForVSCodeTerminal('Cursor', theme);
|
||||
break;
|
||||
result = await installBindingsForVSCodeTerminal('Cursor', theme)
|
||||
break
|
||||
case 'windsurf':
|
||||
result = await installBindingsForVSCodeTerminal('Windsurf', theme);
|
||||
break;
|
||||
result = await installBindingsForVSCodeTerminal('Windsurf', theme)
|
||||
break
|
||||
case 'alacritty':
|
||||
result = await installBindingsForAlacritty(theme);
|
||||
break;
|
||||
result = await installBindingsForAlacritty(theme)
|
||||
break
|
||||
case 'zed':
|
||||
result = await installBindingsForZed(theme);
|
||||
break;
|
||||
result = await installBindingsForZed(theme)
|
||||
break
|
||||
case null:
|
||||
break;
|
||||
break
|
||||
}
|
||||
|
||||
saveGlobalConfig(current => {
|
||||
if (['vscode', 'cursor', 'windsurf', 'alacritty', 'zed'].includes(env.terminal ?? '')) {
|
||||
if (current.shiftEnterKeyBindingInstalled === true) return current;
|
||||
return {
|
||||
...current,
|
||||
shiftEnterKeyBindingInstalled: true
|
||||
};
|
||||
if (
|
||||
['vscode', 'cursor', 'windsurf', 'alacritty', 'zed'].includes(
|
||||
env.terminal ?? '',
|
||||
)
|
||||
) {
|
||||
if (current.shiftEnterKeyBindingInstalled === true) return current
|
||||
return { ...current, shiftEnterKeyBindingInstalled: true }
|
||||
} else if (env.terminal === 'Apple_Terminal') {
|
||||
if (current.optionAsMetaKeyInstalled === true) return current;
|
||||
return {
|
||||
...current,
|
||||
optionAsMetaKeyInstalled: true
|
||||
};
|
||||
if (current.optionAsMetaKeyInstalled === true) return current
|
||||
return { ...current, optionAsMetaKeyInstalled: true }
|
||||
}
|
||||
return current;
|
||||
});
|
||||
maybeMarkProjectOnboardingComplete();
|
||||
return current
|
||||
})
|
||||
|
||||
maybeMarkProjectOnboardingComplete()
|
||||
|
||||
// Install shell completions (ant-only, since the completion command is ant-only)
|
||||
if ((process.env.USER_TYPE) === 'ant') {
|
||||
result += await setupShellCompletion(theme);
|
||||
if (process.env.USER_TYPE === 'ant') {
|
||||
result += await setupShellCompletion(theme)
|
||||
}
|
||||
return result;
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function isShiftEnterKeyBindingInstalled(): boolean {
|
||||
return getGlobalConfig().shiftEnterKeyBindingInstalled === true;
|
||||
return getGlobalConfig().shiftEnterKeyBindingInstalled === true
|
||||
}
|
||||
|
||||
export function hasUsedBackslashReturn(): boolean {
|
||||
return getGlobalConfig().hasUsedBackslashReturn === true;
|
||||
return getGlobalConfig().hasUsedBackslashReturn === true
|
||||
}
|
||||
|
||||
export function markBackslashReturnUsed(): void {
|
||||
const config = getGlobalConfig();
|
||||
const config = getGlobalConfig()
|
||||
if (!config.hasUsedBackslashReturn) {
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
hasUsedBackslashReturn: true
|
||||
}));
|
||||
hasUsedBackslashReturn: true,
|
||||
}))
|
||||
}
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: ToolUseContext & LocalJSXCommandContext, _args: string): Promise<null> {
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: ToolUseContext & LocalJSXCommandContext,
|
||||
_args: string,
|
||||
): Promise<null> {
|
||||
if (env.terminal && env.terminal in NATIVE_CSIU_TERMINALS) {
|
||||
const message = `Shift+Enter is natively supported in ${NATIVE_CSIU_TERMINALS[env.terminal]}.
|
||||
|
||||
No configuration needed. Just use Shift+Enter to add newlines.`;
|
||||
onDone(message);
|
||||
return null;
|
||||
No configuration needed. Just use Shift+Enter to add newlines.`
|
||||
onDone(message)
|
||||
return null
|
||||
}
|
||||
|
||||
// Check if terminal is supported
|
||||
if (!shouldOfferTerminalSetup()) {
|
||||
const terminalName = env.terminal || 'your current terminal';
|
||||
const currentPlatform = getPlatform();
|
||||
const terminalName = env.terminal || 'your current terminal'
|
||||
const currentPlatform = getPlatform()
|
||||
|
||||
// Build platform-specific terminal suggestions
|
||||
let platformTerminals = '';
|
||||
let platformTerminals = ''
|
||||
if (currentPlatform === 'macos') {
|
||||
platformTerminals = ' • macOS: Apple Terminal\n';
|
||||
platformTerminals = ' • macOS: Apple Terminal\n'
|
||||
} else if (currentPlatform === 'windows') {
|
||||
platformTerminals = ' • Windows: Windows Terminal\n';
|
||||
platformTerminals = ' • Windows: Windows Terminal\n'
|
||||
}
|
||||
// For Linux and other platforms, we don't show native terminal options
|
||||
// since they're not currently supported
|
||||
@@ -175,356 +211,482 @@ ${platformTerminals} • IDE: VSCode, Cursor, Windsurf, Zed
|
||||
• Other: Alacritty
|
||||
3. Return to tmux/screen - settings will persist
|
||||
|
||||
${chalk.dim('Note: iTerm2, WezTerm, Ghostty, Kitty, and Warp support Shift+Enter natively.')}`;
|
||||
onDone(message);
|
||||
return null;
|
||||
${chalk.dim('Note: iTerm2, WezTerm, Ghostty, Kitty, and Warp support Shift+Enter natively.')}`
|
||||
onDone(message)
|
||||
return null
|
||||
}
|
||||
const result = await setupTerminal(context.options.theme);
|
||||
onDone(result);
|
||||
return null;
|
||||
|
||||
const result = await setupTerminal(context.options.theme)
|
||||
onDone(result)
|
||||
return null
|
||||
}
|
||||
|
||||
type VSCodeKeybinding = {
|
||||
key: string;
|
||||
command: string;
|
||||
args: {
|
||||
text: string;
|
||||
};
|
||||
when: string;
|
||||
};
|
||||
async function installBindingsForVSCodeTerminal(editor: 'VSCode' | 'Cursor' | 'Windsurf' = 'VSCode', theme: ThemeName): Promise<string> {
|
||||
key: string
|
||||
command: string
|
||||
args: { text: string }
|
||||
when: string
|
||||
}
|
||||
|
||||
async function installBindingsForVSCodeTerminal(
|
||||
editor: 'VSCode' | 'Cursor' | 'Windsurf' = 'VSCode',
|
||||
theme: ThemeName,
|
||||
): Promise<string> {
|
||||
// Check if we're running in a VSCode Remote SSH session
|
||||
// In this case, keybindings need to be installed on the LOCAL machine
|
||||
if (isVSCodeRemoteSSH()) {
|
||||
return `${color('warning', theme)(`Cannot install keybindings from a remote ${editor} session.`)}${EOL}${EOL}${editor} keybindings must be installed on your local machine, not the remote server.${EOL}${EOL}To install the Shift+Enter keybinding:${EOL}1. Open ${editor} on your local machine (not connected to remote)${EOL}2. Open the Command Palette (Cmd/Ctrl+Shift+P) → "Preferences: Open Keyboard Shortcuts (JSON)"${EOL}3. Add this keybinding (the file must be a JSON array):${EOL}${EOL}${chalk.dim(`[
|
||||
return `${color(
|
||||
'warning',
|
||||
theme,
|
||||
)(
|
||||
`Cannot install keybindings from a remote ${editor} session.`,
|
||||
)}${EOL}${EOL}${editor} keybindings must be installed on your local machine, not the remote server.${EOL}${EOL}To install the Shift+Enter keybinding:${EOL}1. Open ${editor} on your local machine (not connected to remote)${EOL}2. Open the Command Palette (Cmd/Ctrl+Shift+P) → "Preferences: Open Keyboard Shortcuts (JSON)"${EOL}3. Add this keybinding (the file must be a JSON array):${EOL}${EOL}${chalk.dim(`[
|
||||
{
|
||||
"key": "shift+enter",
|
||||
"command": "workbench.action.terminal.sendSequence",
|
||||
"args": { "text": "\\u001b\\r" },
|
||||
"when": "terminalFocus"
|
||||
}
|
||||
]`)}${EOL}`;
|
||||
]`)}${EOL}`
|
||||
}
|
||||
const editorDir = editor === 'VSCode' ? 'Code' : editor;
|
||||
const userDirPath = join(homedir(), platform() === 'win32' ? join('AppData', 'Roaming', editorDir, 'User') : platform() === 'darwin' ? join('Library', 'Application Support', editorDir, 'User') : join('.config', editorDir, 'User'));
|
||||
const keybindingsPath = join(userDirPath, 'keybindings.json');
|
||||
|
||||
const editorDir = editor === 'VSCode' ? 'Code' : editor
|
||||
const userDirPath = join(
|
||||
homedir(),
|
||||
platform() === 'win32'
|
||||
? join('AppData', 'Roaming', editorDir, 'User')
|
||||
: platform() === 'darwin'
|
||||
? join('Library', 'Application Support', editorDir, 'User')
|
||||
: join('.config', editorDir, 'User'),
|
||||
)
|
||||
const keybindingsPath = join(userDirPath, 'keybindings.json')
|
||||
|
||||
try {
|
||||
// Ensure user directory exists (idempotent with recursive)
|
||||
await mkdir(userDirPath, {
|
||||
recursive: true
|
||||
});
|
||||
await mkdir(userDirPath, { recursive: true })
|
||||
|
||||
// Read existing keybindings file, or default to empty array if it doesn't exist
|
||||
let content = '[]';
|
||||
let keybindings: VSCodeKeybinding[] = [];
|
||||
let fileExists = false;
|
||||
let content = '[]'
|
||||
let keybindings: VSCodeKeybinding[] = []
|
||||
let fileExists = false
|
||||
try {
|
||||
content = await readFile(keybindingsPath, {
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
fileExists = true;
|
||||
keybindings = safeParseJSONC(content) as VSCodeKeybinding[] ?? [];
|
||||
content = await readFile(keybindingsPath, { encoding: 'utf-8' })
|
||||
fileExists = true
|
||||
keybindings = (safeParseJSONC(content) as VSCodeKeybinding[]) ?? []
|
||||
} catch (e: unknown) {
|
||||
if (!isFsInaccessible(e)) throw e;
|
||||
if (!isFsInaccessible(e)) throw e
|
||||
}
|
||||
|
||||
// Backup the existing file before modifying it
|
||||
if (fileExists) {
|
||||
const randomSha = randomBytes(4).toString('hex');
|
||||
const backupPath = `${keybindingsPath}.${randomSha}.bak`;
|
||||
const randomSha = randomBytes(4).toString('hex')
|
||||
const backupPath = `${keybindingsPath}.${randomSha}.bak`
|
||||
try {
|
||||
await copyFile(keybindingsPath, backupPath);
|
||||
await copyFile(keybindingsPath, backupPath)
|
||||
} catch {
|
||||
return `${color('warning', theme)(`Error backing up existing ${editor} terminal keybindings. Bailing out.`)}${EOL}${chalk.dim(`See ${formatPathLink(keybindingsPath)}`)}${EOL}${chalk.dim(`Backup path: ${formatPathLink(backupPath)}`)}${EOL}`;
|
||||
return `${color(
|
||||
'warning',
|
||||
theme,
|
||||
)(
|
||||
`Error backing up existing ${editor} terminal keybindings. Bailing out.`,
|
||||
)}${EOL}${chalk.dim(`See ${formatPathLink(keybindingsPath)}`)}${EOL}${chalk.dim(`Backup path: ${formatPathLink(backupPath)}`)}${EOL}`
|
||||
}
|
||||
}
|
||||
|
||||
// Check if keybinding already exists
|
||||
const existingBinding = keybindings.find(binding => binding.key === 'shift+enter' && binding.command === 'workbench.action.terminal.sendSequence' && binding.when === 'terminalFocus');
|
||||
const existingBinding = keybindings.find(
|
||||
binding =>
|
||||
binding.key === 'shift+enter' &&
|
||||
binding.command === 'workbench.action.terminal.sendSequence' &&
|
||||
binding.when === 'terminalFocus',
|
||||
)
|
||||
if (existingBinding) {
|
||||
return `${color('warning', theme)(`Found existing ${editor} terminal Shift+Enter key binding. Remove it to continue.`)}${EOL}${chalk.dim(`See ${formatPathLink(keybindingsPath)}`)}${EOL}`;
|
||||
return `${color(
|
||||
'warning',
|
||||
theme,
|
||||
)(
|
||||
`Found existing ${editor} terminal Shift+Enter key binding. Remove it to continue.`,
|
||||
)}${EOL}${chalk.dim(`See ${formatPathLink(keybindingsPath)}`)}${EOL}`
|
||||
}
|
||||
|
||||
// Create the new keybinding
|
||||
const newKeybinding: VSCodeKeybinding = {
|
||||
key: 'shift+enter',
|
||||
command: 'workbench.action.terminal.sendSequence',
|
||||
args: {
|
||||
text: '\u001b\r'
|
||||
},
|
||||
when: 'terminalFocus'
|
||||
};
|
||||
args: { text: '\u001b\r' },
|
||||
when: 'terminalFocus',
|
||||
}
|
||||
|
||||
// Modify the content by adding the new keybinding while preserving comments and formatting
|
||||
const updatedContent = addItemToJSONCArray(content, newKeybinding);
|
||||
const updatedContent = addItemToJSONCArray(content, newKeybinding)
|
||||
|
||||
// Write the updated content back to the file
|
||||
await writeFile(keybindingsPath, updatedContent, {
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
return `${color('success', theme)(`Installed ${editor} terminal Shift+Enter key binding`)}${EOL}${chalk.dim(`See ${formatPathLink(keybindingsPath)}`)}${EOL}`;
|
||||
await writeFile(keybindingsPath, updatedContent, { encoding: 'utf-8' })
|
||||
|
||||
return `${color(
|
||||
'success',
|
||||
theme,
|
||||
)(
|
||||
`Installed ${editor} terminal Shift+Enter key binding`,
|
||||
)}${EOL}${chalk.dim(`See ${formatPathLink(keybindingsPath)}`)}${EOL}`
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
throw new Error(`Failed to install ${editor} terminal Shift+Enter key binding`);
|
||||
logError(error)
|
||||
throw new Error(
|
||||
`Failed to install ${editor} terminal Shift+Enter key binding`,
|
||||
)
|
||||
}
|
||||
}
|
||||
async function enableOptionAsMetaForProfile(profileName: string): Promise<boolean> {
|
||||
|
||||
async function enableOptionAsMetaForProfile(
|
||||
profileName: string,
|
||||
): Promise<boolean> {
|
||||
// First try to add the property (in case it doesn't exist)
|
||||
// Quote the profile name to handle names with spaces (e.g., "Man Page", "Red Sands")
|
||||
const {
|
||||
code: addCode
|
||||
} = await execFileNoThrow('/usr/libexec/PlistBuddy', ['-c', `Add :'Window Settings':'${profileName}':useOptionAsMetaKey bool true`, getTerminalPlistPath()]);
|
||||
const { code: addCode } = await execFileNoThrow('/usr/libexec/PlistBuddy', [
|
||||
'-c',
|
||||
`Add :'Window Settings':'${profileName}':useOptionAsMetaKey bool true`,
|
||||
getTerminalPlistPath(),
|
||||
])
|
||||
|
||||
// If adding fails (likely because it already exists), try setting it instead
|
||||
if (addCode !== 0) {
|
||||
const {
|
||||
code: setCode
|
||||
} = await execFileNoThrow('/usr/libexec/PlistBuddy', ['-c', `Set :'Window Settings':'${profileName}':useOptionAsMetaKey true`, getTerminalPlistPath()]);
|
||||
const { code: setCode } = await execFileNoThrow('/usr/libexec/PlistBuddy', [
|
||||
'-c',
|
||||
`Set :'Window Settings':'${profileName}':useOptionAsMetaKey true`,
|
||||
getTerminalPlistPath(),
|
||||
])
|
||||
|
||||
if (setCode !== 0) {
|
||||
logError(new Error(`Failed to enable Option as Meta key for Terminal.app profile: ${profileName}`));
|
||||
return false;
|
||||
logError(
|
||||
new Error(
|
||||
`Failed to enable Option as Meta key for Terminal.app profile: ${profileName}`,
|
||||
),
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
return true
|
||||
}
|
||||
async function disableAudioBellForProfile(profileName: string): Promise<boolean> {
|
||||
|
||||
async function disableAudioBellForProfile(
|
||||
profileName: string,
|
||||
): Promise<boolean> {
|
||||
// First try to add the property (in case it doesn't exist)
|
||||
// Quote the profile name to handle names with spaces (e.g., "Man Page", "Red Sands")
|
||||
const {
|
||||
code: addCode
|
||||
} = await execFileNoThrow('/usr/libexec/PlistBuddy', ['-c', `Add :'Window Settings':'${profileName}':Bell bool false`, getTerminalPlistPath()]);
|
||||
const { code: addCode } = await execFileNoThrow('/usr/libexec/PlistBuddy', [
|
||||
'-c',
|
||||
`Add :'Window Settings':'${profileName}':Bell bool false`,
|
||||
getTerminalPlistPath(),
|
||||
])
|
||||
|
||||
// If adding fails (likely because it already exists), try setting it instead
|
||||
if (addCode !== 0) {
|
||||
const {
|
||||
code: setCode
|
||||
} = await execFileNoThrow('/usr/libexec/PlistBuddy', ['-c', `Set :'Window Settings':'${profileName}':Bell false`, getTerminalPlistPath()]);
|
||||
const { code: setCode } = await execFileNoThrow('/usr/libexec/PlistBuddy', [
|
||||
'-c',
|
||||
`Set :'Window Settings':'${profileName}':Bell false`,
|
||||
getTerminalPlistPath(),
|
||||
])
|
||||
|
||||
if (setCode !== 0) {
|
||||
logError(new Error(`Failed to disable audio bell for Terminal.app profile: ${profileName}`));
|
||||
return false;
|
||||
logError(
|
||||
new Error(
|
||||
`Failed to disable audio bell for Terminal.app profile: ${profileName}`,
|
||||
),
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Enable Option as Meta key for Terminal.app
|
||||
async function enableOptionAsMetaForTerminal(theme: ThemeName): Promise<string> {
|
||||
async function enableOptionAsMetaForTerminal(
|
||||
theme: ThemeName,
|
||||
): Promise<string> {
|
||||
try {
|
||||
// Create a backup of the current plist file
|
||||
const backupPath = await backupTerminalPreferences();
|
||||
const backupPath = await backupTerminalPreferences()
|
||||
if (!backupPath) {
|
||||
throw new Error('Failed to create backup of Terminal.app preferences, bailing out');
|
||||
throw new Error(
|
||||
'Failed to create backup of Terminal.app preferences, bailing out',
|
||||
)
|
||||
}
|
||||
|
||||
// Read the current default profile from the plist
|
||||
const {
|
||||
stdout: defaultProfile,
|
||||
code: readCode
|
||||
} = await execFileNoThrow('defaults', ['read', 'com.apple.Terminal', 'Default Window Settings']);
|
||||
const { stdout: defaultProfile, code: readCode } = await execFileNoThrow(
|
||||
'defaults',
|
||||
['read', 'com.apple.Terminal', 'Default Window Settings'],
|
||||
)
|
||||
|
||||
if (readCode !== 0 || !defaultProfile.trim()) {
|
||||
throw new Error('Failed to read default Terminal.app profile');
|
||||
throw new Error('Failed to read default Terminal.app profile')
|
||||
}
|
||||
const {
|
||||
stdout: startupProfile,
|
||||
code: startupCode
|
||||
} = await execFileNoThrow('defaults', ['read', 'com.apple.Terminal', 'Startup Window Settings']);
|
||||
|
||||
const { stdout: startupProfile, code: startupCode } = await execFileNoThrow(
|
||||
'defaults',
|
||||
['read', 'com.apple.Terminal', 'Startup Window Settings'],
|
||||
)
|
||||
if (startupCode !== 0 || !startupProfile.trim()) {
|
||||
throw new Error('Failed to read startup Terminal.app profile');
|
||||
throw new Error('Failed to read startup Terminal.app profile')
|
||||
}
|
||||
let wasAnyProfileUpdated = false;
|
||||
const defaultProfileName = defaultProfile.trim();
|
||||
const optionAsMetaEnabled = await enableOptionAsMetaForProfile(defaultProfileName);
|
||||
const audioBellDisabled = await disableAudioBellForProfile(defaultProfileName);
|
||||
|
||||
let wasAnyProfileUpdated = false
|
||||
|
||||
const defaultProfileName = defaultProfile.trim()
|
||||
const optionAsMetaEnabled =
|
||||
await enableOptionAsMetaForProfile(defaultProfileName)
|
||||
const audioBellDisabled =
|
||||
await disableAudioBellForProfile(defaultProfileName)
|
||||
|
||||
if (optionAsMetaEnabled || audioBellDisabled) {
|
||||
wasAnyProfileUpdated = true;
|
||||
wasAnyProfileUpdated = true
|
||||
}
|
||||
const startupProfileName = startupProfile.trim();
|
||||
|
||||
const startupProfileName = startupProfile.trim()
|
||||
|
||||
// Only proceed if the startup profile is different from the default profile
|
||||
if (startupProfileName !== defaultProfileName) {
|
||||
const startupOptionAsMetaEnabled = await enableOptionAsMetaForProfile(startupProfileName);
|
||||
const startupAudioBellDisabled = await disableAudioBellForProfile(startupProfileName);
|
||||
const startupOptionAsMetaEnabled =
|
||||
await enableOptionAsMetaForProfile(startupProfileName)
|
||||
const startupAudioBellDisabled =
|
||||
await disableAudioBellForProfile(startupProfileName)
|
||||
|
||||
if (startupOptionAsMetaEnabled || startupAudioBellDisabled) {
|
||||
wasAnyProfileUpdated = true;
|
||||
wasAnyProfileUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!wasAnyProfileUpdated) {
|
||||
throw new Error('Failed to enable Option as Meta key or disable audio bell for any Terminal.app profile');
|
||||
throw new Error(
|
||||
'Failed to enable Option as Meta key or disable audio bell for any Terminal.app profile',
|
||||
)
|
||||
}
|
||||
|
||||
// Flush the preferences cache
|
||||
await execFileNoThrow('killall', ['cfprefsd']);
|
||||
markTerminalSetupComplete();
|
||||
return `${color('success', theme)(`Configured Terminal.app settings:`)}${EOL}${color('success', theme)('- Enabled "Use Option as Meta key"')}${EOL}${color('success', theme)('- Switched to visual bell')}${EOL}${chalk.dim('Option+Enter will now enter a newline.')}${EOL}${chalk.dim('You must restart Terminal.app for changes to take effect.', theme)}${EOL}`;
|
||||
await execFileNoThrow('killall', ['cfprefsd'])
|
||||
|
||||
markTerminalSetupComplete()
|
||||
|
||||
return `${color(
|
||||
'success',
|
||||
theme,
|
||||
)(
|
||||
`Configured Terminal.app settings:`,
|
||||
)}${EOL}${color('success', theme)('- Enabled "Use Option as Meta key"')}${EOL}${color('success', theme)('- Switched to visual bell')}${EOL}${chalk.dim('Option+Enter will now enter a newline.')}${EOL}${chalk.dim('You must restart Terminal.app for changes to take effect.', theme)}${EOL}`
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
logError(error)
|
||||
|
||||
// Attempt to restore from backup
|
||||
const restoreResult = await checkAndRestoreTerminalBackup();
|
||||
const errorMessage = 'Failed to enable Option as Meta key for Terminal.app.';
|
||||
const restoreResult = await checkAndRestoreTerminalBackup()
|
||||
|
||||
const errorMessage = 'Failed to enable Option as Meta key for Terminal.app.'
|
||||
if (restoreResult.status === 'restored') {
|
||||
throw new Error(`${errorMessage} Your settings have been restored from backup.`);
|
||||
throw new Error(
|
||||
`${errorMessage} Your settings have been restored from backup.`,
|
||||
)
|
||||
} else if (restoreResult.status === 'failed') {
|
||||
throw new Error(`${errorMessage} Restoring from backup failed, try manually with: defaults import com.apple.Terminal ${restoreResult.backupPath}`);
|
||||
throw new Error(
|
||||
`${errorMessage} Restoring from backup failed, try manually with: defaults import com.apple.Terminal ${restoreResult.backupPath}`,
|
||||
)
|
||||
} else {
|
||||
throw new Error(`${errorMessage} No backup was available to restore from.`);
|
||||
throw new Error(
|
||||
`${errorMessage} No backup was available to restore from.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function installBindingsForAlacritty(theme: ThemeName): Promise<string> {
|
||||
const ALACRITTY_KEYBINDING = `[[keyboard.bindings]]
|
||||
key = "Return"
|
||||
mods = "Shift"
|
||||
chars = "\\u001B\\r"`;
|
||||
chars = "\\u001B\\r"`
|
||||
|
||||
// Get Alacritty config file paths in order of preference
|
||||
const configPaths: string[] = [];
|
||||
const configPaths: string[] = []
|
||||
|
||||
// XDG config path (Linux and macOS)
|
||||
const xdgConfigHome = process.env.XDG_CONFIG_HOME;
|
||||
const xdgConfigHome = process.env.XDG_CONFIG_HOME
|
||||
if (xdgConfigHome) {
|
||||
configPaths.push(join(xdgConfigHome, 'alacritty', 'alacritty.toml'));
|
||||
configPaths.push(join(xdgConfigHome, 'alacritty', 'alacritty.toml'))
|
||||
} else {
|
||||
configPaths.push(join(homedir(), '.config', 'alacritty', 'alacritty.toml'));
|
||||
configPaths.push(join(homedir(), '.config', 'alacritty', 'alacritty.toml'))
|
||||
}
|
||||
|
||||
// Windows-specific path
|
||||
if (platform() === 'win32') {
|
||||
const appData = process.env.APPDATA;
|
||||
const appData = process.env.APPDATA
|
||||
if (appData) {
|
||||
configPaths.push(join(appData, 'alacritty', 'alacritty.toml'));
|
||||
configPaths.push(join(appData, 'alacritty', 'alacritty.toml'))
|
||||
}
|
||||
}
|
||||
|
||||
// Find existing config file by attempting to read it, or use first preferred path
|
||||
let configPath: string | null = null;
|
||||
let configContent = '';
|
||||
let configExists = false;
|
||||
let configPath: string | null = null
|
||||
let configContent = ''
|
||||
let configExists = false
|
||||
|
||||
for (const path of configPaths) {
|
||||
try {
|
||||
configContent = await readFile(path, {
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
configPath = path;
|
||||
configExists = true;
|
||||
break;
|
||||
configContent = await readFile(path, { encoding: 'utf-8' })
|
||||
configPath = path
|
||||
configExists = true
|
||||
break
|
||||
} catch (e: unknown) {
|
||||
if (!isFsInaccessible(e)) throw e;
|
||||
if (!isFsInaccessible(e)) throw e
|
||||
// File missing or inaccessible — try next config path
|
||||
}
|
||||
}
|
||||
|
||||
// If no config exists, use the first path (XDG/default location)
|
||||
if (!configPath) {
|
||||
configPath = configPaths[0] ?? null;
|
||||
configPath = configPaths[0] ?? null
|
||||
}
|
||||
|
||||
if (!configPath) {
|
||||
throw new Error('No valid config path found for Alacritty');
|
||||
throw new Error('No valid config path found for Alacritty')
|
||||
}
|
||||
|
||||
try {
|
||||
if (configExists) {
|
||||
// Check if keybinding already exists (look for Shift+Return binding)
|
||||
if (configContent.includes('mods = "Shift"') && configContent.includes('key = "Return"')) {
|
||||
return `${color('warning', theme)('Found existing Alacritty Shift+Enter key binding. Remove it to continue.')}${EOL}${chalk.dim(`See ${formatPathLink(configPath)}`)}${EOL}`;
|
||||
if (
|
||||
configContent.includes('mods = "Shift"') &&
|
||||
configContent.includes('key = "Return"')
|
||||
) {
|
||||
return `${color(
|
||||
'warning',
|
||||
theme,
|
||||
)(
|
||||
'Found existing Alacritty Shift+Enter key binding. Remove it to continue.',
|
||||
)}${EOL}${chalk.dim(`See ${formatPathLink(configPath)}`)}${EOL}`
|
||||
}
|
||||
|
||||
// Create backup
|
||||
const randomSha = randomBytes(4).toString('hex');
|
||||
const backupPath = `${configPath}.${randomSha}.bak`;
|
||||
const randomSha = randomBytes(4).toString('hex')
|
||||
const backupPath = `${configPath}.${randomSha}.bak`
|
||||
try {
|
||||
await copyFile(configPath, backupPath);
|
||||
await copyFile(configPath, backupPath)
|
||||
} catch {
|
||||
return `${color('warning', theme)('Error backing up existing Alacritty config. Bailing out.')}${EOL}${chalk.dim(`See ${formatPathLink(configPath)}`)}${EOL}${chalk.dim(`Backup path: ${formatPathLink(backupPath)}`)}${EOL}`;
|
||||
return `${color(
|
||||
'warning',
|
||||
theme,
|
||||
)(
|
||||
'Error backing up existing Alacritty config. Bailing out.',
|
||||
)}${EOL}${chalk.dim(`See ${formatPathLink(configPath)}`)}${EOL}${chalk.dim(`Backup path: ${formatPathLink(backupPath)}`)}${EOL}`
|
||||
}
|
||||
} else {
|
||||
// Ensure config directory exists (idempotent with recursive)
|
||||
await mkdir(dirname(configPath), {
|
||||
recursive: true
|
||||
});
|
||||
await mkdir(dirname(configPath), { recursive: true })
|
||||
}
|
||||
|
||||
// Add the keybinding to the config
|
||||
let updatedContent = configContent;
|
||||
let updatedContent = configContent
|
||||
if (configContent && !configContent.endsWith('\n')) {
|
||||
updatedContent += '\n';
|
||||
updatedContent += '\n'
|
||||
}
|
||||
updatedContent += '\n' + ALACRITTY_KEYBINDING + '\n';
|
||||
updatedContent += '\n' + ALACRITTY_KEYBINDING + '\n'
|
||||
|
||||
// Write the updated config
|
||||
await writeFile(configPath, updatedContent, {
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
return `${color('success', theme)('Installed Alacritty Shift+Enter key binding')}${EOL}${color('success', theme)('You may need to restart Alacritty for changes to take effect')}${EOL}${chalk.dim(`See ${formatPathLink(configPath)}`)}${EOL}`;
|
||||
await writeFile(configPath, updatedContent, { encoding: 'utf-8' })
|
||||
|
||||
return `${color(
|
||||
'success',
|
||||
theme,
|
||||
)('Installed Alacritty Shift+Enter key binding')}${EOL}${color(
|
||||
'success',
|
||||
theme,
|
||||
)(
|
||||
'You may need to restart Alacritty for changes to take effect',
|
||||
)}${EOL}${chalk.dim(`See ${formatPathLink(configPath)}`)}${EOL}`
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
throw new Error('Failed to install Alacritty Shift+Enter key binding');
|
||||
logError(error)
|
||||
throw new Error('Failed to install Alacritty Shift+Enter key binding')
|
||||
}
|
||||
}
|
||||
|
||||
async function installBindingsForZed(theme: ThemeName): Promise<string> {
|
||||
// Zed uses JSON keybindings similar to VSCode
|
||||
const zedDir = join(homedir(), '.config', 'zed');
|
||||
const keymapPath = join(zedDir, 'keymap.json');
|
||||
const zedDir = join(homedir(), '.config', 'zed')
|
||||
const keymapPath = join(zedDir, 'keymap.json')
|
||||
|
||||
try {
|
||||
// Ensure zed directory exists (idempotent with recursive)
|
||||
await mkdir(zedDir, {
|
||||
recursive: true
|
||||
});
|
||||
await mkdir(zedDir, { recursive: true })
|
||||
|
||||
// Read existing keymap file, or default to empty array if it doesn't exist
|
||||
let keymapContent = '[]';
|
||||
let fileExists = false;
|
||||
let keymapContent = '[]'
|
||||
let fileExists = false
|
||||
try {
|
||||
keymapContent = await readFile(keymapPath, {
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
fileExists = true;
|
||||
keymapContent = await readFile(keymapPath, { encoding: 'utf-8' })
|
||||
fileExists = true
|
||||
} catch (e: unknown) {
|
||||
if (!isFsInaccessible(e)) throw e;
|
||||
if (!isFsInaccessible(e)) throw e
|
||||
}
|
||||
|
||||
if (fileExists) {
|
||||
// Check if keybinding already exists
|
||||
if (keymapContent.includes('shift-enter')) {
|
||||
return `${color('warning', theme)('Found existing Zed Shift+Enter key binding. Remove it to continue.')}${EOL}${chalk.dim(`See ${formatPathLink(keymapPath)}`)}${EOL}`;
|
||||
return `${color(
|
||||
'warning',
|
||||
theme,
|
||||
)(
|
||||
'Found existing Zed Shift+Enter key binding. Remove it to continue.',
|
||||
)}${EOL}${chalk.dim(`See ${formatPathLink(keymapPath)}`)}${EOL}`
|
||||
}
|
||||
|
||||
// Create backup
|
||||
const randomSha = randomBytes(4).toString('hex');
|
||||
const backupPath = `${keymapPath}.${randomSha}.bak`;
|
||||
const randomSha = randomBytes(4).toString('hex')
|
||||
const backupPath = `${keymapPath}.${randomSha}.bak`
|
||||
try {
|
||||
await copyFile(keymapPath, backupPath);
|
||||
await copyFile(keymapPath, backupPath)
|
||||
} catch {
|
||||
return `${color('warning', theme)('Error backing up existing Zed keymap. Bailing out.')}${EOL}${chalk.dim(`See ${formatPathLink(keymapPath)}`)}${EOL}${chalk.dim(`Backup path: ${formatPathLink(backupPath)}`)}${EOL}`;
|
||||
return `${color(
|
||||
'warning',
|
||||
theme,
|
||||
)(
|
||||
'Error backing up existing Zed keymap. Bailing out.',
|
||||
)}${EOL}${chalk.dim(`See ${formatPathLink(keymapPath)}`)}${EOL}${chalk.dim(`Backup path: ${formatPathLink(backupPath)}`)}${EOL}`
|
||||
}
|
||||
}
|
||||
|
||||
// Parse and modify the keymap
|
||||
let keymap: Array<{
|
||||
context?: string;
|
||||
bindings: Record<string, string | string[]>;
|
||||
}>;
|
||||
context?: string
|
||||
bindings: Record<string, string | string[]>
|
||||
}>
|
||||
try {
|
||||
keymap = jsonParse(keymapContent);
|
||||
keymap = jsonParse(keymapContent)
|
||||
if (!Array.isArray(keymap)) {
|
||||
keymap = [];
|
||||
keymap = []
|
||||
}
|
||||
} catch {
|
||||
keymap = [];
|
||||
keymap = []
|
||||
}
|
||||
|
||||
// Add the new keybinding for terminal context
|
||||
keymap.push({
|
||||
context: 'Terminal',
|
||||
bindings: {
|
||||
'shift-enter': ['terminal::SendText', '\u001b\r']
|
||||
}
|
||||
});
|
||||
'shift-enter': ['terminal::SendText', '\u001b\r'],
|
||||
},
|
||||
})
|
||||
|
||||
// Write the updated keymap
|
||||
await writeFile(keymapPath, jsonStringify(keymap, null, 2) + '\n', {
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
return `${color('success', theme)('Installed Zed Shift+Enter key binding')}${EOL}${chalk.dim(`See ${formatPathLink(keymapPath)}`)}${EOL}`;
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
|
||||
return `${color(
|
||||
'success',
|
||||
theme,
|
||||
)(
|
||||
'Installed Zed Shift+Enter key binding',
|
||||
)}${EOL}${chalk.dim(`See ${formatPathLink(keymapPath)}`)}${EOL}`
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
throw new Error('Failed to install Zed Shift+Enter key binding');
|
||||
logError(error)
|
||||
throw new Error('Failed to install Zed Shift+Enter key binding')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,36 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import { Pane } from '../../components/design-system/Pane.js';
|
||||
import { ThemePicker } from '../../components/ThemePicker.js';
|
||||
import { useTheme } from '../../ink.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import * as React from 'react'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import { Pane } from '../../components/design-system/Pane.js'
|
||||
import { ThemePicker } from '../../components/ThemePicker.js'
|
||||
import { useTheme } from '../../ink.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
|
||||
type Props = {
|
||||
onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
};
|
||||
function ThemePickerCommand(t0) {
|
||||
const $ = _c(8);
|
||||
const {
|
||||
onDone
|
||||
} = t0;
|
||||
const [, setTheme] = useTheme();
|
||||
let t1;
|
||||
if ($[0] !== onDone || $[1] !== setTheme) {
|
||||
t1 = setting => {
|
||||
setTheme(setting);
|
||||
onDone(`Theme set to ${setting}`);
|
||||
};
|
||||
$[0] = onDone;
|
||||
$[1] = setTheme;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== onDone) {
|
||||
t2 = () => {
|
||||
onDone("Theme picker dismissed", {
|
||||
display: "system"
|
||||
});
|
||||
};
|
||||
$[3] = onDone;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
let t3;
|
||||
if ($[5] !== t1 || $[6] !== t2) {
|
||||
t3 = <Pane color="permission"><ThemePicker onThemeSelect={t1} onCancel={t2} skipExitHandling={true} /></Pane>;
|
||||
$[5] = t1;
|
||||
$[6] = t2;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
return t3;
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
}
|
||||
|
||||
function ThemePickerCommand({ onDone }: Props): React.ReactNode {
|
||||
const [, setTheme] = useTheme()
|
||||
|
||||
return (
|
||||
<Pane color="permission">
|
||||
<ThemePicker
|
||||
onThemeSelect={setting => {
|
||||
setTheme(setting)
|
||||
onDone(`Theme set to ${setting}`)
|
||||
}}
|
||||
onCancel={() => {
|
||||
onDone('Theme picker dismissed', { display: 'system' })
|
||||
}}
|
||||
skipExitHandling={true}
|
||||
/>
|
||||
</Pane>
|
||||
)
|
||||
}
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, _context) => {
|
||||
return <ThemePickerCommand onDone={onDone} />;
|
||||
};
|
||||
return <ThemePickerCommand onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,67 +1,84 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { execa } from 'execa';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import { Select } from '../../components/CustomSelect/select.js';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { Spinner } from '../../components/Spinner.js';
|
||||
import instances from '../../ink/instances.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { enablePluginOp } from '../../services/plugins/pluginOperations.js';
|
||||
import { logForDebugging } from '../../utils/debug.js';
|
||||
import { isENOENT, toError } from '../../utils/errors.js';
|
||||
import { execFileNoThrow } from '../../utils/execFileNoThrow.js';
|
||||
import { pathExists } from '../../utils/file.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import { getPlatform } from '../../utils/platform.js';
|
||||
import { clearAllCaches } from '../../utils/plugins/cacheUtils.js';
|
||||
import { isPluginInstalled } from '../../utils/plugins/installedPluginsManager.js';
|
||||
import { addMarketplaceSource, clearMarketplacesCache, loadKnownMarketplacesConfig, refreshMarketplace } from '../../utils/plugins/marketplaceManager.js';
|
||||
import { OFFICIAL_MARKETPLACE_NAME } from '../../utils/plugins/officialMarketplace.js';
|
||||
import { loadAllPlugins } from '../../utils/plugins/pluginLoader.js';
|
||||
import { installSelectedPlugins } from '../../utils/plugins/pluginStartupCheck.js';
|
||||
import { execa } from 'execa'
|
||||
import { readFile } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import { Select } from '../../components/CustomSelect/select.js'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { Spinner } from '../../components/Spinner.js'
|
||||
import instances from '../../ink/instances.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { enablePluginOp } from '../../services/plugins/pluginOperations.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
import { isENOENT, toError } from '../../utils/errors.js'
|
||||
import { execFileNoThrow } from '../../utils/execFileNoThrow.js'
|
||||
import { pathExists } from '../../utils/file.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import { getPlatform } from '../../utils/platform.js'
|
||||
import { clearAllCaches } from '../../utils/plugins/cacheUtils.js'
|
||||
import { isPluginInstalled } from '../../utils/plugins/installedPluginsManager.js'
|
||||
import {
|
||||
addMarketplaceSource,
|
||||
clearMarketplacesCache,
|
||||
loadKnownMarketplacesConfig,
|
||||
refreshMarketplace,
|
||||
} from '../../utils/plugins/marketplaceManager.js'
|
||||
import { OFFICIAL_MARKETPLACE_NAME } from '../../utils/plugins/officialMarketplace.js'
|
||||
import { loadAllPlugins } from '../../utils/plugins/pluginLoader.js'
|
||||
import { installSelectedPlugins } from '../../utils/plugins/pluginStartupCheck.js'
|
||||
|
||||
// Marketplace and plugin identifiers - varies by user type
|
||||
const INTERNAL_MARKETPLACE_NAME = 'claude-code-marketplace';
|
||||
const INTERNAL_MARKETPLACE_REPO = 'anthropics/claude-code-marketplace';
|
||||
const OFFICIAL_MARKETPLACE_REPO = 'anthropics/claude-plugins-official';
|
||||
const INTERNAL_MARKETPLACE_NAME = 'claude-code-marketplace'
|
||||
const INTERNAL_MARKETPLACE_REPO = 'anthropics/claude-code-marketplace'
|
||||
const OFFICIAL_MARKETPLACE_REPO = 'anthropics/claude-plugins-official'
|
||||
|
||||
function getMarketplaceName(): string {
|
||||
return (process.env.USER_TYPE) === 'ant' ? INTERNAL_MARKETPLACE_NAME : OFFICIAL_MARKETPLACE_NAME;
|
||||
return process.env.USER_TYPE === 'ant'
|
||||
? INTERNAL_MARKETPLACE_NAME
|
||||
: OFFICIAL_MARKETPLACE_NAME
|
||||
}
|
||||
|
||||
function getMarketplaceRepo(): string {
|
||||
return (process.env.USER_TYPE) === 'ant' ? INTERNAL_MARKETPLACE_REPO : OFFICIAL_MARKETPLACE_REPO;
|
||||
return process.env.USER_TYPE === 'ant'
|
||||
? INTERNAL_MARKETPLACE_REPO
|
||||
: OFFICIAL_MARKETPLACE_REPO
|
||||
}
|
||||
|
||||
function getPluginId(): string {
|
||||
return `thinkback@${getMarketplaceName()}`;
|
||||
return `thinkback@${getMarketplaceName()}`
|
||||
}
|
||||
const SKILL_NAME = 'thinkback';
|
||||
|
||||
const SKILL_NAME = 'thinkback'
|
||||
|
||||
/**
|
||||
* Get the thinkback skill directory from the installed plugin's cache path
|
||||
*/
|
||||
async function getThinkbackSkillDir(): Promise<string | null> {
|
||||
const {
|
||||
enabled
|
||||
} = await loadAllPlugins();
|
||||
const thinkbackPlugin = enabled.find(p => p.name === 'thinkback' || p.source && p.source.includes(getPluginId()));
|
||||
const { enabled } = await loadAllPlugins()
|
||||
const thinkbackPlugin = enabled.find(
|
||||
p =>
|
||||
p.name === 'thinkback' || (p.source && p.source.includes(getPluginId())),
|
||||
)
|
||||
|
||||
if (!thinkbackPlugin) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const skillDir = join(thinkbackPlugin.path, 'skills', SKILL_NAME);
|
||||
|
||||
const skillDir = join(thinkbackPlugin.path, 'skills', SKILL_NAME)
|
||||
if (await pathExists(skillDir)) {
|
||||
return skillDir;
|
||||
return skillDir
|
||||
}
|
||||
return null;
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export async function playAnimation(skillDir: string): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
success: boolean
|
||||
message: string
|
||||
}> {
|
||||
const dataPath = join(skillDir, 'year_in_review.js');
|
||||
const playerPath = join(skillDir, 'player.js');
|
||||
const dataPath = join(skillDir, 'year_in_review.js')
|
||||
const playerPath = join(skillDir, 'player.js')
|
||||
|
||||
// Both files are prerequisites for the node subprocess. Read them here
|
||||
// (not at call sites) so all callers get consistent error messaging. The
|
||||
@@ -72,482 +89,433 @@ export async function playAnimation(skillDir: string): Promise<{
|
||||
// than thrown — the old pathExists-based code never threw, and one caller
|
||||
// (handleSelect) uses `void playAnimation().then(...)` without a .catch().
|
||||
try {
|
||||
await readFile(dataPath);
|
||||
await readFile(dataPath)
|
||||
} catch (e: unknown) {
|
||||
if (isENOENT(e)) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'No animation found. Run /think-back first to generate one.'
|
||||
};
|
||||
message: 'No animation found. Run /think-back first to generate one.',
|
||||
}
|
||||
}
|
||||
logError(e);
|
||||
logError(e)
|
||||
return {
|
||||
success: false,
|
||||
message: `Could not access animation data: ${toError(e).message}`
|
||||
};
|
||||
message: `Could not access animation data: ${toError(e).message}`,
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await readFile(playerPath);
|
||||
await readFile(playerPath)
|
||||
} catch (e: unknown) {
|
||||
if (isENOENT(e)) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Player script not found. The player.js file is missing from the thinkback skill.'
|
||||
};
|
||||
message:
|
||||
'Player script not found. The player.js file is missing from the thinkback skill.',
|
||||
}
|
||||
}
|
||||
logError(e);
|
||||
logError(e)
|
||||
return {
|
||||
success: false,
|
||||
message: `Could not access player script: ${toError(e).message}`
|
||||
};
|
||||
message: `Could not access player script: ${toError(e).message}`,
|
||||
}
|
||||
}
|
||||
|
||||
// Get ink instance for terminal takeover
|
||||
const inkInstance = instances.get(process.stdout);
|
||||
const inkInstance = instances.get(process.stdout)
|
||||
if (!inkInstance) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to access terminal instance'
|
||||
};
|
||||
return { success: false, message: 'Failed to access terminal instance' }
|
||||
}
|
||||
inkInstance.enterAlternateScreen();
|
||||
|
||||
inkInstance.enterAlternateScreen()
|
||||
try {
|
||||
await execa('node', [playerPath], {
|
||||
stdio: 'inherit',
|
||||
cwd: skillDir,
|
||||
reject: false
|
||||
});
|
||||
reject: false,
|
||||
})
|
||||
} catch {
|
||||
// Animation may have been interrupted (e.g., Ctrl+C)
|
||||
} finally {
|
||||
inkInstance.exitAlternateScreen();
|
||||
inkInstance.exitAlternateScreen()
|
||||
}
|
||||
|
||||
// Open the HTML file in browser for video download
|
||||
const htmlPath = join(skillDir, 'year_in_review.html');
|
||||
const htmlPath = join(skillDir, 'year_in_review.html')
|
||||
if (await pathExists(htmlPath)) {
|
||||
const platform = getPlatform();
|
||||
const openCmd = platform === 'macos' ? 'open' : platform === 'windows' ? 'start' : 'xdg-open';
|
||||
void execFileNoThrow(openCmd, [htmlPath]);
|
||||
const platform = getPlatform()
|
||||
const openCmd =
|
||||
platform === 'macos'
|
||||
? 'open'
|
||||
: platform === 'windows'
|
||||
? 'start'
|
||||
: 'xdg-open'
|
||||
void execFileNoThrow(openCmd, [htmlPath])
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
message: 'Year in review animation complete!'
|
||||
};
|
||||
|
||||
return { success: true, message: 'Year in review animation complete!' }
|
||||
}
|
||||
type InstallState = {
|
||||
phase: 'checking';
|
||||
} | {
|
||||
phase: 'installing-marketplace';
|
||||
} | {
|
||||
phase: 'installing-plugin';
|
||||
} | {
|
||||
phase: 'enabling-plugin';
|
||||
} | {
|
||||
phase: 'ready';
|
||||
} | {
|
||||
phase: 'error';
|
||||
message: string;
|
||||
};
|
||||
|
||||
type InstallState =
|
||||
| { phase: 'checking' }
|
||||
| { phase: 'installing-marketplace' }
|
||||
| { phase: 'installing-plugin' }
|
||||
| { phase: 'enabling-plugin' }
|
||||
| { phase: 'ready' }
|
||||
| { phase: 'error'; message: string }
|
||||
|
||||
function ThinkbackInstaller({
|
||||
onReady,
|
||||
onError
|
||||
onError,
|
||||
}: {
|
||||
onReady: () => void;
|
||||
onError: (message: string) => void;
|
||||
onReady: () => void
|
||||
onError: (message: string) => void
|
||||
}): React.ReactNode {
|
||||
const [state, setState] = useState<InstallState>({
|
||||
phase: 'checking'
|
||||
});
|
||||
const [progressMessage, setProgressMessage] = useState('');
|
||||
const [state, setState] = useState<InstallState>({ phase: 'checking' })
|
||||
const [progressMessage, setProgressMessage] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
async function checkAndInstall(): Promise<void> {
|
||||
try {
|
||||
// Check if marketplace is installed
|
||||
const knownMarketplaces = await loadKnownMarketplacesConfig();
|
||||
const marketplaceName = getMarketplaceName();
|
||||
const marketplaceRepo = getMarketplaceRepo();
|
||||
const pluginId = getPluginId();
|
||||
const marketplaceInstalled = marketplaceName in knownMarketplaces;
|
||||
const knownMarketplaces = await loadKnownMarketplacesConfig()
|
||||
const marketplaceName = getMarketplaceName()
|
||||
const marketplaceRepo = getMarketplaceRepo()
|
||||
const pluginId = getPluginId()
|
||||
const marketplaceInstalled = marketplaceName in knownMarketplaces
|
||||
|
||||
// Check if plugin is already installed first
|
||||
const pluginAlreadyInstalled = isPluginInstalled(pluginId);
|
||||
const pluginAlreadyInstalled = isPluginInstalled(pluginId)
|
||||
|
||||
if (!marketplaceInstalled) {
|
||||
// Install the marketplace
|
||||
setState({
|
||||
phase: 'installing-marketplace'
|
||||
});
|
||||
logForDebugging(`Installing marketplace ${marketplaceRepo}`);
|
||||
await addMarketplaceSource({
|
||||
source: 'github',
|
||||
repo: marketplaceRepo
|
||||
}, message => {
|
||||
setProgressMessage(message);
|
||||
});
|
||||
clearAllCaches();
|
||||
logForDebugging(`Marketplace ${marketplaceName} installed`);
|
||||
setState({ phase: 'installing-marketplace' })
|
||||
logForDebugging(`Installing marketplace ${marketplaceRepo}`)
|
||||
|
||||
await addMarketplaceSource(
|
||||
{ source: 'github', repo: marketplaceRepo },
|
||||
message => {
|
||||
setProgressMessage(message)
|
||||
},
|
||||
)
|
||||
clearAllCaches()
|
||||
logForDebugging(`Marketplace ${marketplaceName} installed`)
|
||||
} else if (!pluginAlreadyInstalled) {
|
||||
// Marketplace installed but plugin not installed - refresh to get latest plugins
|
||||
// Only refresh when needed to avoid potentially destructive git operations
|
||||
setState({
|
||||
phase: 'installing-marketplace'
|
||||
});
|
||||
setProgressMessage('Updating marketplace…');
|
||||
logForDebugging(`Refreshing marketplace ${marketplaceName}`);
|
||||
await refreshMarketplace(marketplaceName, message_0 => {
|
||||
setProgressMessage(message_0);
|
||||
});
|
||||
clearMarketplacesCache();
|
||||
clearAllCaches();
|
||||
logForDebugging(`Marketplace ${marketplaceName} refreshed`);
|
||||
setState({ phase: 'installing-marketplace' })
|
||||
setProgressMessage('Updating marketplace…')
|
||||
logForDebugging(`Refreshing marketplace ${marketplaceName}`)
|
||||
|
||||
await refreshMarketplace(marketplaceName, message => {
|
||||
setProgressMessage(message)
|
||||
})
|
||||
clearMarketplacesCache()
|
||||
clearAllCaches()
|
||||
logForDebugging(`Marketplace ${marketplaceName} refreshed`)
|
||||
}
|
||||
|
||||
if (!pluginAlreadyInstalled) {
|
||||
// Install the plugin
|
||||
setState({
|
||||
phase: 'installing-plugin'
|
||||
});
|
||||
logForDebugging(`Installing plugin ${pluginId}`);
|
||||
const result = await installSelectedPlugins([pluginId]);
|
||||
setState({ phase: 'installing-plugin' })
|
||||
logForDebugging(`Installing plugin ${pluginId}`)
|
||||
|
||||
const result = await installSelectedPlugins([pluginId])
|
||||
|
||||
if (result.failed.length > 0) {
|
||||
const errorMsg = result.failed.map(f => `${f.name}: ${f.error}`).join(', ');
|
||||
throw new Error(`Failed to install plugin: ${errorMsg}`);
|
||||
const errorMsg = result.failed
|
||||
.map(f => `${f.name}: ${f.error}`)
|
||||
.join(', ')
|
||||
throw new Error(`Failed to install plugin: ${errorMsg}`)
|
||||
}
|
||||
clearAllCaches();
|
||||
logForDebugging(`Plugin ${pluginId} installed`);
|
||||
|
||||
clearAllCaches()
|
||||
logForDebugging(`Plugin ${pluginId} installed`)
|
||||
} else {
|
||||
// Plugin is installed, check if it's enabled
|
||||
const {
|
||||
disabled
|
||||
} = await loadAllPlugins();
|
||||
const isDisabled = disabled.some(p => p.name === 'thinkback' || p.source?.includes(pluginId));
|
||||
const { disabled } = await loadAllPlugins()
|
||||
const isDisabled = disabled.some(
|
||||
p => p.name === 'thinkback' || p.source?.includes(pluginId),
|
||||
)
|
||||
|
||||
if (isDisabled) {
|
||||
// Enable the plugin
|
||||
setState({
|
||||
phase: 'enabling-plugin'
|
||||
});
|
||||
logForDebugging(`Enabling plugin ${pluginId}`);
|
||||
const enableResult = await enablePluginOp(pluginId);
|
||||
setState({ phase: 'enabling-plugin' })
|
||||
logForDebugging(`Enabling plugin ${pluginId}`)
|
||||
|
||||
const enableResult = await enablePluginOp(pluginId)
|
||||
if (!enableResult.success) {
|
||||
throw new Error(`Failed to enable plugin: ${enableResult.message}`);
|
||||
throw new Error(
|
||||
`Failed to enable plugin: ${enableResult.message}`,
|
||||
)
|
||||
}
|
||||
clearAllCaches();
|
||||
logForDebugging(`Plugin ${pluginId} enabled`);
|
||||
|
||||
clearAllCaches()
|
||||
logForDebugging(`Plugin ${pluginId} enabled`)
|
||||
}
|
||||
}
|
||||
setState({
|
||||
phase: 'ready'
|
||||
});
|
||||
onReady();
|
||||
|
||||
setState({ phase: 'ready' })
|
||||
onReady()
|
||||
} catch (error) {
|
||||
const err = toError(error);
|
||||
logError(err);
|
||||
setState({
|
||||
phase: 'error',
|
||||
message: err.message
|
||||
});
|
||||
onError(err.message);
|
||||
const err = toError(error)
|
||||
logError(err)
|
||||
setState({ phase: 'error', message: err.message })
|
||||
onError(err.message)
|
||||
}
|
||||
}
|
||||
void checkAndInstall();
|
||||
}, [onReady, onError]);
|
||||
|
||||
void checkAndInstall()
|
||||
}, [onReady, onError])
|
||||
|
||||
if (state.phase === 'error') {
|
||||
return <Box flexDirection="column">
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color="error">Error: {state.message}</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (state.phase === 'ready') {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const statusMessage = state.phase === 'checking' ? 'Checking thinkback installation…' : state.phase === 'installing-marketplace' ? 'Installing marketplace…' : state.phase === 'enabling-plugin' ? 'Enabling thinkback plugin…' : 'Installing thinkback plugin…';
|
||||
return <Box flexDirection="column">
|
||||
|
||||
const statusMessage =
|
||||
state.phase === 'checking'
|
||||
? 'Checking thinkback installation…'
|
||||
: state.phase === 'installing-marketplace'
|
||||
? 'Installing marketplace…'
|
||||
: state.phase === 'enabling-plugin'
|
||||
? 'Enabling thinkback plugin…'
|
||||
: 'Installing thinkback plugin…'
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text>{progressMessage || statusMessage}</Text>
|
||||
</Box>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
type MenuAction = 'play' | 'edit' | 'fix' | 'regenerate';
|
||||
type GenerativeAction = Exclude<MenuAction, 'play'>;
|
||||
function ThinkbackMenu(t0) {
|
||||
const $ = _c(19);
|
||||
const {
|
||||
onDone,
|
||||
onAction,
|
||||
skillDir,
|
||||
hasGenerated
|
||||
} = t0;
|
||||
const [hasSelected, setHasSelected] = useState(false);
|
||||
let t1;
|
||||
if ($[0] !== hasGenerated) {
|
||||
t1 = hasGenerated ? [{
|
||||
label: "Play animation",
|
||||
value: "play" as const,
|
||||
description: "Watch your year in review"
|
||||
}, {
|
||||
label: "Edit content",
|
||||
value: "edit" as const,
|
||||
description: "Modify the animation"
|
||||
}, {
|
||||
label: "Fix errors",
|
||||
value: "fix" as const,
|
||||
description: "Fix validation or rendering issues"
|
||||
}, {
|
||||
label: "Regenerate",
|
||||
value: "regenerate" as const,
|
||||
description: "Create a new animation from scratch"
|
||||
}] : [{
|
||||
label: "Let's go!",
|
||||
value: "regenerate" as const,
|
||||
description: "Generate your personalized animation"
|
||||
}];
|
||||
$[0] = hasGenerated;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
|
||||
type MenuAction = 'play' | 'edit' | 'fix' | 'regenerate'
|
||||
type GenerativeAction = Exclude<MenuAction, 'play'>
|
||||
|
||||
function ThinkbackMenu({
|
||||
onDone,
|
||||
onAction,
|
||||
skillDir,
|
||||
hasGenerated,
|
||||
}: {
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay; shouldQuery?: boolean },
|
||||
) => void
|
||||
onAction: (action: GenerativeAction) => void
|
||||
skillDir: string
|
||||
hasGenerated: boolean
|
||||
}): React.ReactNode {
|
||||
const [hasSelected, setHasSelected] = useState(false)
|
||||
|
||||
const options = hasGenerated
|
||||
? [
|
||||
{
|
||||
label: 'Play animation',
|
||||
value: 'play' as const,
|
||||
description: 'Watch your year in review',
|
||||
},
|
||||
{
|
||||
label: 'Edit content',
|
||||
value: 'edit' as const,
|
||||
description: 'Modify the animation',
|
||||
},
|
||||
{
|
||||
label: 'Fix errors',
|
||||
value: 'fix' as const,
|
||||
description: 'Fix validation or rendering issues',
|
||||
},
|
||||
{
|
||||
label: 'Regenerate',
|
||||
value: 'regenerate' as const,
|
||||
description: 'Create a new animation from scratch',
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
label: "Let's go!",
|
||||
value: 'regenerate' as const,
|
||||
description: 'Generate your personalized animation',
|
||||
},
|
||||
]
|
||||
|
||||
function handleSelect(value: MenuAction): void {
|
||||
setHasSelected(true)
|
||||
if (value === 'play') {
|
||||
// Play runs the terminal-takeover animation, then signal done with skip
|
||||
void playAnimation(skillDir).then(() => {
|
||||
onDone(undefined, { display: 'skip' })
|
||||
})
|
||||
} else {
|
||||
onAction(value)
|
||||
}
|
||||
}
|
||||
const options = t1;
|
||||
let t2;
|
||||
if ($[2] !== onAction || $[3] !== onDone || $[4] !== skillDir) {
|
||||
t2 = function handleSelect(value) {
|
||||
setHasSelected(true);
|
||||
if (value === "play") {
|
||||
playAnimation(skillDir).then(() => {
|
||||
onDone(undefined, {
|
||||
display: "skip"
|
||||
});
|
||||
});
|
||||
} else {
|
||||
onAction(value);
|
||||
}
|
||||
};
|
||||
$[2] = onAction;
|
||||
$[3] = onDone;
|
||||
$[4] = skillDir;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
|
||||
function handleCancel(): void {
|
||||
onDone(undefined, { display: 'skip' })
|
||||
}
|
||||
const handleSelect = t2;
|
||||
let t3;
|
||||
if ($[6] !== onDone) {
|
||||
t3 = function handleCancel() {
|
||||
onDone(undefined, {
|
||||
display: "skip"
|
||||
});
|
||||
};
|
||||
$[6] = onDone;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
const handleCancel = t3;
|
||||
|
||||
if (hasSelected) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
let t4;
|
||||
if ($[8] !== hasGenerated) {
|
||||
t4 = !hasGenerated && <Box flexDirection="column"><Text>Relive your year of coding with Claude.</Text><Text dimColor={true}>{"We'll create a personalized ASCII animation celebrating your journey."}</Text></Box>;
|
||||
$[8] = hasGenerated;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
let t5;
|
||||
if ($[10] !== handleSelect || $[11] !== options) {
|
||||
t5 = <Select options={options} onChange={handleSelect} visibleOptionCount={5} />;
|
||||
$[10] = handleSelect;
|
||||
$[11] = options;
|
||||
$[12] = t5;
|
||||
} else {
|
||||
t5 = $[12];
|
||||
}
|
||||
let t6;
|
||||
if ($[13] !== t4 || $[14] !== t5) {
|
||||
t6 = <Box flexDirection="column" gap={1}>{t4}{t5}</Box>;
|
||||
$[13] = t4;
|
||||
$[14] = t5;
|
||||
$[15] = t6;
|
||||
} else {
|
||||
t6 = $[15];
|
||||
}
|
||||
let t7;
|
||||
if ($[16] !== handleCancel || $[17] !== t6) {
|
||||
t7 = <Dialog title="Think Back on 2025 with Claude Code" subtitle="Generate your 2025 Claude Code Think Back (takes a few minutes to run)" onCancel={handleCancel} color="claude">{t6}</Dialog>;
|
||||
$[16] = handleCancel;
|
||||
$[17] = t6;
|
||||
$[18] = t7;
|
||||
} else {
|
||||
t7 = $[18];
|
||||
}
|
||||
return t7;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="Think Back on 2025 with Claude Code"
|
||||
subtitle="Generate your 2025 Claude Code Think Back (takes a few minutes to run)"
|
||||
onCancel={handleCancel}
|
||||
color="claude"
|
||||
>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
{/* Description for first-time users */}
|
||||
{!hasGenerated && (
|
||||
<Box flexDirection="column">
|
||||
<Text>Relive your year of coding with Claude.</Text>
|
||||
<Text dimColor>
|
||||
{
|
||||
"We'll create a personalized ASCII animation celebrating your journey."
|
||||
}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Menu */}
|
||||
<Select
|
||||
options={options}
|
||||
onChange={handleSelect}
|
||||
visibleOptionCount={5}
|
||||
/>
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
const EDIT_PROMPT = 'Use the Skill tool to invoke the "thinkback" skill with mode=edit to modify my existing Claude Code year in review animation. Ask me what I want to change. When the animation is ready, tell the user to run /think-back again to play it.';
|
||||
const FIX_PROMPT = 'Use the Skill tool to invoke the "thinkback" skill with mode=fix to fix validation or rendering errors in my existing Claude Code year in review animation. Run the validator, identify errors, and fix them. When the animation is ready, tell the user to run /think-back again to play it.';
|
||||
const REGENERATE_PROMPT = 'Use the Skill tool to invoke the "thinkback" skill with mode=regenerate to create a completely new Claude Code year in review animation from scratch. Delete the existing animation and start fresh. When the animation is ready, tell the user to run /think-back again to play it.';
|
||||
function ThinkbackFlow(t0) {
|
||||
const $ = _c(27);
|
||||
const {
|
||||
onDone
|
||||
} = t0;
|
||||
const [installComplete, setInstallComplete] = useState(false);
|
||||
const [installError, setInstallError] = useState(null);
|
||||
const [skillDir, setSkillDir] = useState(null);
|
||||
const [hasGenerated, setHasGenerated] = useState(null);
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = function handleReady() {
|
||||
setInstallComplete(true);
|
||||
};
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
|
||||
const EDIT_PROMPT =
|
||||
'Use the Skill tool to invoke the "thinkback" skill with mode=edit to modify my existing Claude Code year in review animation. Ask me what I want to change. When the animation is ready, tell the user to run /think-back again to play it.'
|
||||
|
||||
const FIX_PROMPT =
|
||||
'Use the Skill tool to invoke the "thinkback" skill with mode=fix to fix validation or rendering errors in my existing Claude Code year in review animation. Run the validator, identify errors, and fix them. When the animation is ready, tell the user to run /think-back again to play it.'
|
||||
|
||||
const REGENERATE_PROMPT =
|
||||
'Use the Skill tool to invoke the "thinkback" skill with mode=regenerate to create a completely new Claude Code year in review animation from scratch. Delete the existing animation and start fresh. When the animation is ready, tell the user to run /think-back again to play it.'
|
||||
|
||||
function ThinkbackFlow({
|
||||
onDone,
|
||||
}: {
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay; shouldQuery?: boolean },
|
||||
) => void
|
||||
}): React.ReactNode {
|
||||
const [installComplete, setInstallComplete] = useState(false)
|
||||
const [installError, setInstallError] = useState<string | null>(null)
|
||||
const [skillDir, setSkillDir] = useState<string | null>(null)
|
||||
const [hasGenerated, setHasGenerated] = useState<boolean | null>(null)
|
||||
|
||||
function handleReady(): void {
|
||||
setInstallComplete(true)
|
||||
}
|
||||
const handleReady = t1;
|
||||
let t2;
|
||||
if ($[1] !== onDone) {
|
||||
t2 = message => {
|
||||
setInstallError(message);
|
||||
onDone(`Error with thinkback: ${message}. Try running /plugin to manually install the think-back plugin.`, {
|
||||
display: "system"
|
||||
});
|
||||
};
|
||||
$[1] = onDone;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
|
||||
const handleError = useCallback(
|
||||
(message: string): void => {
|
||||
setInstallError(message)
|
||||
// Call onDone with the error message so the model can continue
|
||||
onDone(
|
||||
`Error with thinkback: ${message}. Try running /plugin to manually install the think-back plugin.`,
|
||||
{ display: 'system' },
|
||||
)
|
||||
},
|
||||
[onDone],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (installComplete && !skillDir && !installError) {
|
||||
// Get the skill directory after installation
|
||||
void getThinkbackSkillDir().then(dir => {
|
||||
if (dir) {
|
||||
logForDebugging(`Thinkback skill directory: ${dir}`)
|
||||
setSkillDir(dir)
|
||||
} else {
|
||||
handleError('Could not find thinkback skill directory')
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [installComplete, skillDir, installError, handleError])
|
||||
|
||||
// Check for generated file once we have skillDir
|
||||
useEffect(() => {
|
||||
if (!skillDir) {
|
||||
return
|
||||
}
|
||||
|
||||
const dataPath = join(skillDir, 'year_in_review.js')
|
||||
void pathExists(dataPath).then(exists => {
|
||||
logForDebugging(
|
||||
`Checking for ${dataPath}: ${exists ? 'found' : 'not found'}`,
|
||||
)
|
||||
setHasGenerated(exists)
|
||||
})
|
||||
}, [skillDir])
|
||||
|
||||
function handleAction(action: GenerativeAction): void {
|
||||
// Send prompt to model based on action
|
||||
const prompts: Record<GenerativeAction, string> = {
|
||||
edit: EDIT_PROMPT,
|
||||
fix: FIX_PROMPT,
|
||||
regenerate: REGENERATE_PROMPT,
|
||||
}
|
||||
onDone(prompts[action], { display: 'user', shouldQuery: true })
|
||||
}
|
||||
const handleError = t2;
|
||||
let t3;
|
||||
let t4;
|
||||
if ($[3] !== handleError || $[4] !== installComplete || $[5] !== installError || $[6] !== skillDir) {
|
||||
t3 = () => {
|
||||
if (installComplete && !skillDir && !installError) {
|
||||
getThinkbackSkillDir().then(dir => {
|
||||
if (dir) {
|
||||
logForDebugging(`Thinkback skill directory: ${dir}`);
|
||||
setSkillDir(dir);
|
||||
} else {
|
||||
handleError("Could not find thinkback skill directory");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
t4 = [installComplete, skillDir, installError, handleError];
|
||||
$[3] = handleError;
|
||||
$[4] = installComplete;
|
||||
$[5] = installError;
|
||||
$[6] = skillDir;
|
||||
$[7] = t3;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
t4 = $[8];
|
||||
}
|
||||
useEffect(t3, t4);
|
||||
let t5;
|
||||
let t6;
|
||||
if ($[9] !== skillDir) {
|
||||
t5 = () => {
|
||||
if (!skillDir) {
|
||||
return;
|
||||
}
|
||||
const dataPath = join(skillDir, "year_in_review.js");
|
||||
pathExists(dataPath).then(exists => {
|
||||
logForDebugging(`Checking for ${dataPath}: ${exists ? "found" : "not found"}`);
|
||||
setHasGenerated(exists);
|
||||
});
|
||||
};
|
||||
t6 = [skillDir];
|
||||
$[9] = skillDir;
|
||||
$[10] = t5;
|
||||
$[11] = t6;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
t6 = $[11];
|
||||
}
|
||||
useEffect(t5, t6);
|
||||
let t7;
|
||||
if ($[12] !== onDone) {
|
||||
t7 = function handleAction(action) {
|
||||
const prompts = {
|
||||
edit: EDIT_PROMPT,
|
||||
fix: FIX_PROMPT,
|
||||
regenerate: REGENERATE_PROMPT
|
||||
};
|
||||
onDone(prompts[action], {
|
||||
display: "user",
|
||||
shouldQuery: true
|
||||
});
|
||||
};
|
||||
$[12] = onDone;
|
||||
$[13] = t7;
|
||||
} else {
|
||||
t7 = $[13];
|
||||
}
|
||||
const handleAction = t7;
|
||||
|
||||
if (installError) {
|
||||
let t8;
|
||||
if ($[14] !== installError) {
|
||||
t8 = <Text color="error">Error: {installError}</Text>;
|
||||
$[14] = installError;
|
||||
$[15] = t8;
|
||||
} else {
|
||||
t8 = $[15];
|
||||
}
|
||||
let t9;
|
||||
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = <Text dimColor={true}>Try running /plugin to manually install the think-back plugin.</Text>;
|
||||
$[16] = t9;
|
||||
} else {
|
||||
t9 = $[16];
|
||||
}
|
||||
let t10;
|
||||
if ($[17] !== t8) {
|
||||
t10 = <Box flexDirection="column">{t8}{t9}</Box>;
|
||||
$[17] = t8;
|
||||
$[18] = t10;
|
||||
} else {
|
||||
t10 = $[18];
|
||||
}
|
||||
return t10;
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color="error">Error: {installError}</Text>
|
||||
<Text dimColor>
|
||||
Try running /plugin to manually install the think-back plugin.
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (!installComplete) {
|
||||
let t8;
|
||||
if ($[19] !== handleError) {
|
||||
t8 = <ThinkbackInstaller onReady={handleReady} onError={handleError} />;
|
||||
$[19] = handleError;
|
||||
$[20] = t8;
|
||||
} else {
|
||||
t8 = $[20];
|
||||
}
|
||||
return t8;
|
||||
return <ThinkbackInstaller onReady={handleReady} onError={handleError} />
|
||||
}
|
||||
|
||||
if (!skillDir || hasGenerated === null) {
|
||||
let t8;
|
||||
if ($[21] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = <Box><Spinner /><Text>Loading thinkback skill…</Text></Box>;
|
||||
$[21] = t8;
|
||||
} else {
|
||||
t8 = $[21];
|
||||
}
|
||||
return t8;
|
||||
return (
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text>Loading thinkback skill…</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
let t8;
|
||||
if ($[22] !== handleAction || $[23] !== hasGenerated || $[24] !== onDone || $[25] !== skillDir) {
|
||||
t8 = <ThinkbackMenu onDone={onDone} onAction={handleAction} skillDir={skillDir} hasGenerated={hasGenerated} />;
|
||||
$[22] = handleAction;
|
||||
$[23] = hasGenerated;
|
||||
$[24] = onDone;
|
||||
$[25] = skillDir;
|
||||
$[26] = t8;
|
||||
} else {
|
||||
t8 = $[26];
|
||||
}
|
||||
return t8;
|
||||
|
||||
return (
|
||||
<ThinkbackMenu
|
||||
onDone={onDone}
|
||||
onAction={handleAction}
|
||||
skillDir={skillDir}
|
||||
hasGenerated={hasGenerated}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export async function call(onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
shouldQuery?: boolean;
|
||||
}) => void): Promise<React.ReactNode> {
|
||||
return <ThinkbackFlow onDone={onDone} />;
|
||||
|
||||
export async function call(
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay; shouldQuery?: boolean },
|
||||
) => void,
|
||||
): Promise<React.ReactNode> {
|
||||
return <ThinkbackFlow onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,28 +1,42 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { REMOTE_CONTROL_DISCONNECTED_MSG } from '../bridge/types.js';
|
||||
import type { Command } from '../commands.js';
|
||||
import { DIAMOND_OPEN } from '../constants/figures.js';
|
||||
import { getRemoteSessionUrl } from '../constants/product.js';
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../services/analytics/index.js';
|
||||
import type { AppState } from '../state/AppStateStore.js';
|
||||
import { checkRemoteAgentEligibility, formatPreconditionError, RemoteAgentTask, type RemoteAgentTaskState, registerRemoteAgentTask } from '../tasks/RemoteAgentTask/RemoteAgentTask.js';
|
||||
import type { LocalJSXCommandCall } from '../types/command.js';
|
||||
import { logForDebugging } from '../utils/debug.js';
|
||||
import { errorMessage } from '../utils/errors.js';
|
||||
import { logError } from '../utils/log.js';
|
||||
import { enqueuePendingNotification } from '../utils/messageQueueManager.js';
|
||||
import { ALL_MODEL_CONFIGS } from '../utils/model/configs.js';
|
||||
import { updateTaskState } from '../utils/task/framework.js';
|
||||
import { archiveRemoteSession, teleportToRemote } from '../utils/teleport.js';
|
||||
import { pollForApprovedExitPlanMode, UltraplanPollError } from '../utils/ultraplan/ccrSession.js';
|
||||
import { readFileSync } from 'fs'
|
||||
import { REMOTE_CONTROL_DISCONNECTED_MSG } from '../bridge/types.js'
|
||||
import type { Command } from '../commands.js'
|
||||
import { DIAMOND_OPEN } from '../constants/figures.js'
|
||||
import { getRemoteSessionUrl } from '../constants/product.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../services/analytics/index.js'
|
||||
import type { AppState } from '../state/AppStateStore.js'
|
||||
import {
|
||||
checkRemoteAgentEligibility,
|
||||
formatPreconditionError,
|
||||
RemoteAgentTask,
|
||||
type RemoteAgentTaskState,
|
||||
registerRemoteAgentTask,
|
||||
} from '../tasks/RemoteAgentTask/RemoteAgentTask.js'
|
||||
import type { LocalJSXCommandCall } from '../types/command.js'
|
||||
import { logForDebugging } from '../utils/debug.js'
|
||||
import { errorMessage } from '../utils/errors.js'
|
||||
import { logError } from '../utils/log.js'
|
||||
import { enqueuePendingNotification } from '../utils/messageQueueManager.js'
|
||||
import { ALL_MODEL_CONFIGS } from '../utils/model/configs.js'
|
||||
import { updateTaskState } from '../utils/task/framework.js'
|
||||
import { archiveRemoteSession, teleportToRemote } from '../utils/teleport.js'
|
||||
import {
|
||||
pollForApprovedExitPlanMode,
|
||||
UltraplanPollError,
|
||||
} from '../utils/ultraplan/ccrSession.js'
|
||||
|
||||
// TODO(prod-hardening): OAuth token may go stale over the 30min poll;
|
||||
// consider refresh.
|
||||
|
||||
// Multi-agent exploration is slow; 30min timeout.
|
||||
const ULTRAPLAN_TIMEOUT_MS = 30 * 60 * 1000;
|
||||
export const CCR_TERMS_URL = 'https://code.claude.com/docs/en/claude-code-on-the-web';
|
||||
const ULTRAPLAN_TIMEOUT_MS = 30 * 60 * 1000
|
||||
|
||||
export const CCR_TERMS_URL =
|
||||
'https://code.claude.com/docs/en/claude-code-on-the-web'
|
||||
|
||||
// CCR runs against the first-party API — use the canonical ID, not the
|
||||
// provider-specific string getModelStrings() would return (which may be a
|
||||
@@ -30,7 +44,10 @@ export const CCR_TERMS_URL = 'https://code.claude.com/docs/en/claude-code-on-the
|
||||
// load: the GrowthBook cache is empty at import and `/config` Gates can flip
|
||||
// it between invocations.
|
||||
function getUltraplanModel(): string {
|
||||
return getFeatureValue_CACHED_MAY_BE_STALE('tengu_ultraplan_model', ALL_MODEL_CONFIGS.opus46.firstParty);
|
||||
return getFeatureValue_CACHED_MAY_BE_STALE(
|
||||
'tengu_ultraplan_model',
|
||||
ALL_MODEL_CONFIGS.opus46.firstParty,
|
||||
)
|
||||
}
|
||||
|
||||
// prompt.txt is wrapped in <system-reminder> so the CCR browser hides
|
||||
@@ -43,9 +60,11 @@ function getUltraplanModel(): string {
|
||||
//
|
||||
// Bundler inlines .txt as a string; the test runner wraps it as {default}.
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const _rawPrompt = require('../utils/ultraplan/prompt.txt');
|
||||
const _rawPrompt = require('../utils/ultraplan/prompt.txt')
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
const DEFAULT_INSTRUCTIONS: string = (typeof _rawPrompt === 'string' ? _rawPrompt : _rawPrompt.default).trimEnd();
|
||||
const DEFAULT_INSTRUCTIONS: string = (
|
||||
typeof _rawPrompt === 'string' ? _rawPrompt : _rawPrompt.default
|
||||
).trimEnd()
|
||||
|
||||
// Dev-only prompt override resolved eagerly at module load.
|
||||
// Gated to ant builds (USER_TYPE is a build-time define,
|
||||
@@ -53,7 +72,10 @@ const DEFAULT_INSTRUCTIONS: string = (typeof _rawPrompt === 'string' ? _rawPromp
|
||||
// Shell-set env only, so top-level process.env read is fine
|
||||
// — settings.env never injects this.
|
||||
/* eslint-disable custom-rules/no-process-env-top-level, custom-rules/no-sync-fs -- ant-only dev override; eager top-level read is the point (crash at startup, not silently inside the slash-command try/catch) */
|
||||
const ULTRAPLAN_INSTRUCTIONS: string = (process.env.USER_TYPE) === 'ant' && process.env.ULTRAPLAN_PROMPT_FILE ? readFileSync(process.env.ULTRAPLAN_PROMPT_FILE, 'utf8').trimEnd() : DEFAULT_INSTRUCTIONS;
|
||||
const ULTRAPLAN_INSTRUCTIONS: string =
|
||||
process.env.USER_TYPE === 'ant' && process.env.ULTRAPLAN_PROMPT_FILE
|
||||
? readFileSync(process.env.ULTRAPLAN_PROMPT_FILE, 'utf8').trimEnd()
|
||||
: DEFAULT_INSTRUCTIONS
|
||||
/* eslint-enable custom-rules/no-process-env-top-level, custom-rules/no-sync-fs */
|
||||
|
||||
/**
|
||||
@@ -61,107 +83,123 @@ const ULTRAPLAN_INSTRUCTIONS: string = (process.env.USER_TYPE) === 'ant' && proc
|
||||
* system-reminder so the browser renders them; scaffolding is hidden.
|
||||
*/
|
||||
export function buildUltraplanPrompt(blurb: string, seedPlan?: string): string {
|
||||
const parts: string[] = [];
|
||||
const parts: string[] = []
|
||||
if (seedPlan) {
|
||||
parts.push('Here is a draft plan to refine:', '', seedPlan, '');
|
||||
parts.push('Here is a draft plan to refine:', '', seedPlan, '')
|
||||
}
|
||||
parts.push(ULTRAPLAN_INSTRUCTIONS);
|
||||
parts.push(ULTRAPLAN_INSTRUCTIONS)
|
||||
if (blurb) {
|
||||
parts.push('', blurb);
|
||||
parts.push('', blurb)
|
||||
}
|
||||
return parts.join('\n');
|
||||
return parts.join('\n')
|
||||
}
|
||||
function startDetachedPoll(taskId: string, sessionId: string, url: string, getAppState: () => AppState, setAppState: (f: (prev: AppState) => AppState) => void): void {
|
||||
const started = Date.now();
|
||||
let failed = false;
|
||||
|
||||
function startDetachedPoll(
|
||||
taskId: string,
|
||||
sessionId: string,
|
||||
url: string,
|
||||
getAppState: () => AppState,
|
||||
setAppState: (f: (prev: AppState) => AppState) => void,
|
||||
): void {
|
||||
const started = Date.now()
|
||||
let failed = false
|
||||
void (async () => {
|
||||
try {
|
||||
const {
|
||||
plan,
|
||||
rejectCount,
|
||||
executionTarget
|
||||
} = await pollForApprovedExitPlanMode(sessionId, ULTRAPLAN_TIMEOUT_MS, phase => {
|
||||
if (phase === 'needs_input') logEvent('tengu_ultraplan_awaiting_input', {});
|
||||
updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t => {
|
||||
if (t.status !== 'running') return t;
|
||||
const next = phase === 'running' ? undefined : phase;
|
||||
return t.ultraplanPhase === next ? t : {
|
||||
...t,
|
||||
ultraplanPhase: next
|
||||
};
|
||||
});
|
||||
}, () => getAppState().tasks?.[taskId]?.status !== 'running');
|
||||
const { plan, rejectCount, executionTarget } =
|
||||
await pollForApprovedExitPlanMode(
|
||||
sessionId,
|
||||
ULTRAPLAN_TIMEOUT_MS,
|
||||
phase => {
|
||||
if (phase === 'needs_input')
|
||||
logEvent('tengu_ultraplan_awaiting_input', {})
|
||||
updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t => {
|
||||
if (t.status !== 'running') return t
|
||||
const next = phase === 'running' ? undefined : phase
|
||||
return t.ultraplanPhase === next
|
||||
? t
|
||||
: { ...t, ultraplanPhase: next }
|
||||
})
|
||||
},
|
||||
() => getAppState().tasks?.[taskId]?.status !== 'running',
|
||||
)
|
||||
logEvent('tengu_ultraplan_approved', {
|
||||
duration_ms: Date.now() - started,
|
||||
plan_length: plan.length,
|
||||
reject_count: rejectCount,
|
||||
execution_target: executionTarget as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
execution_target:
|
||||
executionTarget as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
if (executionTarget === 'remote') {
|
||||
// User chose "execute in CCR" in the browser PlanModal — the remote
|
||||
// session is now coding. Skip archive (ARCHIVE has no running-check,
|
||||
// would kill mid-execution) and skip the choice dialog (already chose).
|
||||
// Guard on task status so a poll that resolves after stopUltraplan
|
||||
// doesn't notify for a killed session.
|
||||
const task = getAppState().tasks?.[taskId];
|
||||
if (task?.status !== 'running') return;
|
||||
updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t => t.status !== 'running' ? t : {
|
||||
...t,
|
||||
status: 'completed',
|
||||
endTime: Date.now()
|
||||
});
|
||||
setAppState(prev => prev.ultraplanSessionUrl === url ? {
|
||||
...prev,
|
||||
ultraplanSessionUrl: undefined
|
||||
} : prev);
|
||||
const task = getAppState().tasks?.[taskId]
|
||||
if (task?.status !== 'running') return
|
||||
updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t =>
|
||||
t.status !== 'running'
|
||||
? t
|
||||
: { ...t, status: 'completed', endTime: Date.now() },
|
||||
)
|
||||
setAppState(prev =>
|
||||
prev.ultraplanSessionUrl === url
|
||||
? { ...prev, ultraplanSessionUrl: undefined }
|
||||
: prev,
|
||||
)
|
||||
enqueuePendingNotification({
|
||||
value: [`Ultraplan approved — executing in Claude Code on the web. Follow along at: ${url}`, '', 'Results will land as a pull request when the remote session finishes. There is nothing to do here.'].join('\n'),
|
||||
mode: 'task-notification'
|
||||
});
|
||||
value: [
|
||||
`Ultraplan approved — executing in Claude Code on the web. Follow along at: ${url}`,
|
||||
'',
|
||||
'Results will land as a pull request when the remote session finishes. There is nothing to do here.',
|
||||
].join('\n'),
|
||||
mode: 'task-notification',
|
||||
})
|
||||
} else {
|
||||
// Teleport: set pendingChoice so REPL mounts UltraplanChoiceDialog.
|
||||
// The dialog owns archive + URL clear on choice. Guard on task status
|
||||
// so a poll that resolves after stopUltraplan doesn't resurrect the
|
||||
// dialog for a killed session.
|
||||
setAppState(prev => {
|
||||
const task = prev.tasks?.[taskId];
|
||||
if (!task || task.status !== 'running') return prev;
|
||||
const task = prev.tasks?.[taskId]
|
||||
if (!task || task.status !== 'running') return prev
|
||||
return {
|
||||
...prev,
|
||||
ultraplanPendingChoice: {
|
||||
plan,
|
||||
sessionId,
|
||||
taskId
|
||||
}
|
||||
};
|
||||
});
|
||||
ultraplanPendingChoice: { plan, sessionId, taskId },
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
// If the task was stopped (stopUltraplan sets status=killed), the poll
|
||||
// erroring is expected — skip the failure notification and cleanup
|
||||
// (kill() already archived; stopUltraplan cleared the URL).
|
||||
const task = getAppState().tasks?.[taskId];
|
||||
if (task?.status !== 'running') return;
|
||||
failed = true;
|
||||
const task = getAppState().tasks?.[taskId]
|
||||
if (task?.status !== 'running') return
|
||||
failed = true
|
||||
logEvent('tengu_ultraplan_failed', {
|
||||
duration_ms: Date.now() - started,
|
||||
reason: (e instanceof UltraplanPollError ? e.reason : 'network_or_unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
reject_count: e instanceof UltraplanPollError ? e.rejectCount : undefined
|
||||
});
|
||||
reason: (e instanceof UltraplanPollError
|
||||
? e.reason
|
||||
: 'network_or_unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
reject_count:
|
||||
e instanceof UltraplanPollError ? e.rejectCount : undefined,
|
||||
})
|
||||
enqueuePendingNotification({
|
||||
value: `Ultraplan failed: ${errorMessage(e)}\n\nSession: ${url}`,
|
||||
mode: 'task-notification'
|
||||
});
|
||||
mode: 'task-notification',
|
||||
})
|
||||
// Error path owns cleanup; teleport path defers to the dialog; remote
|
||||
// path handled its own cleanup above.
|
||||
void archiveRemoteSession(sessionId).catch(e => logForDebugging(`ultraplan archive failed: ${String(e)}`));
|
||||
void archiveRemoteSession(sessionId).catch(e =>
|
||||
logForDebugging(`ultraplan archive failed: ${String(e)}`),
|
||||
)
|
||||
setAppState(prev =>
|
||||
// Compare against this poll's URL so a newer relaunched session's
|
||||
// URL isn't cleared by a stale poll erroring out.
|
||||
prev.ultraplanSessionUrl === url ? {
|
||||
...prev,
|
||||
ultraplanSessionUrl: undefined
|
||||
} : prev);
|
||||
// Compare against this poll's URL so a newer relaunched session's
|
||||
// URL isn't cleared by a stale poll erroring out.
|
||||
prev.ultraplanSessionUrl === url
|
||||
? { ...prev, ultraplanSessionUrl: undefined }
|
||||
: prev,
|
||||
)
|
||||
} finally {
|
||||
// Remote path already set status=completed above; teleport path
|
||||
// leaves status=running so the pill shows the ultraplanPhase state
|
||||
@@ -170,27 +208,31 @@ function startDetachedPoll(taskId: string, sessionId: string, url: string, getAp
|
||||
// isBackgroundTask before the pill can render the phase state.
|
||||
// Failure path has no dialog, so it owns the status transition here.
|
||||
if (failed) {
|
||||
updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t => t.status !== 'running' ? t : {
|
||||
...t,
|
||||
status: 'failed',
|
||||
endTime: Date.now()
|
||||
});
|
||||
updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t =>
|
||||
t.status !== 'running'
|
||||
? t
|
||||
: { ...t, status: 'failed', endTime: Date.now() },
|
||||
)
|
||||
}
|
||||
}
|
||||
})();
|
||||
})()
|
||||
}
|
||||
|
||||
// Renders immediately so the terminal doesn't appear hung during the
|
||||
// multi-second teleportToRemote round-trip.
|
||||
function buildLaunchMessage(disconnectedBridge?: boolean): string {
|
||||
const prefix = disconnectedBridge ? `${REMOTE_CONTROL_DISCONNECTED_MSG} ` : '';
|
||||
return `${DIAMOND_OPEN} ultraplan\n${prefix}Starting Claude Code on the web…`;
|
||||
const prefix = disconnectedBridge ? `${REMOTE_CONTROL_DISCONNECTED_MSG} ` : ''
|
||||
return `${DIAMOND_OPEN} ultraplan\n${prefix}Starting Claude Code on the web…`
|
||||
}
|
||||
|
||||
function buildSessionReadyMessage(url: string): string {
|
||||
return `${DIAMOND_OPEN} ultraplan · Monitor progress in Claude Code on the web ${url}\nYou can continue working — when the ${DIAMOND_OPEN} fills, press ↓ to view results`;
|
||||
return `${DIAMOND_OPEN} ultraplan · Monitor progress in Claude Code on the web ${url}\nYou can continue working — when the ${DIAMOND_OPEN} fills, press ↓ to view results`
|
||||
}
|
||||
|
||||
function buildAlreadyActiveMessage(url: string | undefined): string {
|
||||
return url ? `ultraplan: already polling. Open ${url} to check status, or wait for the plan to land here.` : 'ultraplan: already launching. Please wait for the session to start.';
|
||||
return url
|
||||
? `ultraplan: already polling. Open ${url} to check status, or wait for the plan to land here.`
|
||||
: 'ultraplan: already launching. Please wait for the session to start.'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,26 +242,37 @@ function buildAlreadyActiveMessage(url: string | undefined): string {
|
||||
* shouldStop callback sees the killed status on its next tick and throws;
|
||||
* the catch block early-returns when status !== 'running'.
|
||||
*/
|
||||
export async function stopUltraplan(taskId: string, sessionId: string, setAppState: (f: (prev: AppState) => AppState) => void): Promise<void> {
|
||||
export async function stopUltraplan(
|
||||
taskId: string,
|
||||
sessionId: string,
|
||||
setAppState: (f: (prev: AppState) => AppState) => void,
|
||||
): Promise<void> {
|
||||
// RemoteAgentTask.kill archives the session (with .catch) — no separate
|
||||
// archive call needed here.
|
||||
await RemoteAgentTask.kill(taskId, setAppState);
|
||||
setAppState(prev => prev.ultraplanSessionUrl || prev.ultraplanPendingChoice || prev.ultraplanLaunching ? {
|
||||
...prev,
|
||||
ultraplanSessionUrl: undefined,
|
||||
ultraplanPendingChoice: undefined,
|
||||
ultraplanLaunching: undefined
|
||||
} : prev);
|
||||
const url = getRemoteSessionUrl(sessionId, process.env.SESSION_INGRESS_URL);
|
||||
await RemoteAgentTask.kill(taskId, setAppState)
|
||||
setAppState(prev =>
|
||||
prev.ultraplanSessionUrl ||
|
||||
prev.ultraplanPendingChoice ||
|
||||
prev.ultraplanLaunching
|
||||
? {
|
||||
...prev,
|
||||
ultraplanSessionUrl: undefined,
|
||||
ultraplanPendingChoice: undefined,
|
||||
ultraplanLaunching: undefined,
|
||||
}
|
||||
: prev,
|
||||
)
|
||||
const url = getRemoteSessionUrl(sessionId, process.env.SESSION_INGRESS_URL)
|
||||
enqueuePendingNotification({
|
||||
value: `Ultraplan stopped.\n\nSession: ${url}`,
|
||||
mode: 'task-notification'
|
||||
});
|
||||
enqueuePendingNotification({
|
||||
value: 'The user stopped the ultraplan session above. Do not respond to the stop notification — wait for their next message.',
|
||||
mode: 'task-notification',
|
||||
isMeta: true
|
||||
});
|
||||
})
|
||||
enqueuePendingNotification({
|
||||
value:
|
||||
'The user stopped the ultraplan session above. Do not respond to the stop notification — wait for their next message.',
|
||||
mode: 'task-notification',
|
||||
isMeta: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,13 +285,13 @@ export async function stopUltraplan(taskId: string, sessionId: string, setAppSta
|
||||
* enqueuePendingNotification.
|
||||
*/
|
||||
export async function launchUltraplan(opts: {
|
||||
blurb: string;
|
||||
seedPlan?: string;
|
||||
getAppState: () => AppState;
|
||||
setAppState: (f: (prev: AppState) => AppState) => void;
|
||||
signal: AbortSignal;
|
||||
blurb: string
|
||||
seedPlan?: string
|
||||
getAppState: () => AppState
|
||||
setAppState: (f: (prev: AppState) => AppState) => void
|
||||
signal: AbortSignal
|
||||
/** True if the caller disconnected Remote Control before launching. */
|
||||
disconnectedBridge?: boolean;
|
||||
disconnectedBridge?: boolean
|
||||
/**
|
||||
* Called once teleportToRemote resolves with a session URL. Callers that
|
||||
* have setMessages (REPL) append this as a second transcript message so the
|
||||
@@ -246,7 +299,7 @@ export async function launchUltraplan(opts: {
|
||||
* transcript access (ExitPlanModePermissionRequest) omit this — the pill
|
||||
* still shows live status.
|
||||
*/
|
||||
onSessionReady?: (msg: string) => void;
|
||||
onSessionReady?: (msg: string) => void
|
||||
}): Promise<string> {
|
||||
const {
|
||||
blurb,
|
||||
@@ -255,79 +308,90 @@ export async function launchUltraplan(opts: {
|
||||
setAppState,
|
||||
signal,
|
||||
disconnectedBridge,
|
||||
onSessionReady
|
||||
} = opts;
|
||||
const {
|
||||
ultraplanSessionUrl: active,
|
||||
ultraplanLaunching
|
||||
} = getAppState();
|
||||
onSessionReady,
|
||||
} = opts
|
||||
|
||||
const { ultraplanSessionUrl: active, ultraplanLaunching } = getAppState()
|
||||
if (active || ultraplanLaunching) {
|
||||
logEvent('tengu_ultraplan_create_failed', {
|
||||
reason: (active ? 'already_polling' : 'already_launching') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
return buildAlreadyActiveMessage(active);
|
||||
reason: (active
|
||||
? 'already_polling'
|
||||
: 'already_launching') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
return buildAlreadyActiveMessage(active)
|
||||
}
|
||||
|
||||
if (!blurb && !seedPlan) {
|
||||
// No event — bare /ultraplan is a usage query, not an attempt.
|
||||
return [
|
||||
// Rendered via <Markdown>; raw <message> is tokenized as HTML
|
||||
// and dropped. Backslash-escape the brackets.
|
||||
'Usage: /ultraplan \\<prompt\\>, or include "ultraplan" anywhere', 'in your prompt', '', 'Advanced multi-agent plan mode with our most powerful model', '(Opus). Runs in Claude Code on the web. When the plan is ready,', 'you can execute it in the web session or send it back here.', 'Terminal stays free while the remote plans.', 'Requires /login.', '', `Terms: ${CCR_TERMS_URL}`].join('\n');
|
||||
// Rendered via <Markdown>; raw <message> is tokenized as HTML
|
||||
// and dropped. Backslash-escape the brackets.
|
||||
'Usage: /ultraplan \\<prompt\\>, or include "ultraplan" anywhere',
|
||||
'in your prompt',
|
||||
'',
|
||||
'Advanced multi-agent plan mode with our most powerful model',
|
||||
'(Opus). Runs in Claude Code on the web. When the plan is ready,',
|
||||
'you can execute it in the web session or send it back here.',
|
||||
'Terminal stays free while the remote plans.',
|
||||
'Requires /login.',
|
||||
'',
|
||||
`Terms: ${CCR_TERMS_URL}`,
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
// Set synchronously before the detached flow to prevent duplicate launches
|
||||
// during the teleportToRemote window.
|
||||
setAppState(prev => prev.ultraplanLaunching ? prev : {
|
||||
...prev,
|
||||
ultraplanLaunching: true
|
||||
});
|
||||
setAppState(prev =>
|
||||
prev.ultraplanLaunching ? prev : { ...prev, ultraplanLaunching: true },
|
||||
)
|
||||
void launchDetached({
|
||||
blurb,
|
||||
seedPlan,
|
||||
getAppState,
|
||||
setAppState,
|
||||
signal,
|
||||
onSessionReady
|
||||
});
|
||||
return buildLaunchMessage(disconnectedBridge);
|
||||
onSessionReady,
|
||||
})
|
||||
return buildLaunchMessage(disconnectedBridge)
|
||||
}
|
||||
|
||||
async function launchDetached(opts: {
|
||||
blurb: string;
|
||||
seedPlan?: string;
|
||||
getAppState: () => AppState;
|
||||
setAppState: (f: (prev: AppState) => AppState) => void;
|
||||
signal: AbortSignal;
|
||||
onSessionReady?: (msg: string) => void;
|
||||
blurb: string
|
||||
seedPlan?: string
|
||||
getAppState: () => AppState
|
||||
setAppState: (f: (prev: AppState) => AppState) => void
|
||||
signal: AbortSignal
|
||||
onSessionReady?: (msg: string) => void
|
||||
}): Promise<void> {
|
||||
const {
|
||||
blurb,
|
||||
seedPlan,
|
||||
getAppState,
|
||||
setAppState,
|
||||
signal,
|
||||
onSessionReady
|
||||
} = opts;
|
||||
const { blurb, seedPlan, getAppState, setAppState, signal, onSessionReady } =
|
||||
opts
|
||||
// Hoisted so the catch block can archive the remote session if an error
|
||||
// occurs after teleportToRemote succeeds (avoids 30min orphan).
|
||||
let sessionId: string | undefined;
|
||||
let sessionId: string | undefined
|
||||
try {
|
||||
const model = getUltraplanModel();
|
||||
const eligibility = await checkRemoteAgentEligibility();
|
||||
const model = getUltraplanModel()
|
||||
|
||||
const eligibility = await checkRemoteAgentEligibility()
|
||||
if (!eligibility.eligible) {
|
||||
const eligErrors = (eligibility as { eligible: false; errors: Array<{ type: string }> }).errors;
|
||||
logEvent('tengu_ultraplan_create_failed', {
|
||||
reason: 'precondition' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
precondition_errors: eligErrors.map(e => e.type).join(',') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
const reasons = eligErrors.map(formatPreconditionError).join('\n');
|
||||
reason:
|
||||
'precondition' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
precondition_errors: eligibility.errors
|
||||
.map(e => e.type)
|
||||
.join(
|
||||
',',
|
||||
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
const reasons = eligibility.errors.map(formatPreconditionError).join('\n')
|
||||
enqueuePendingNotification({
|
||||
value: `ultraplan: cannot launch remote session —\n${reasons}`,
|
||||
mode: 'task-notification'
|
||||
});
|
||||
return;
|
||||
mode: 'task-notification',
|
||||
})
|
||||
return
|
||||
}
|
||||
const prompt = buildUltraplanPrompt(blurb, seedPlan);
|
||||
let bundleFailMsg: string | undefined;
|
||||
|
||||
const prompt = buildUltraplanPrompt(blurb, seedPlan)
|
||||
let bundleFailMsg: string | undefined
|
||||
const session = await teleportToRemote({
|
||||
initialMessage: prompt,
|
||||
description: blurb || 'Refine local plan',
|
||||
@@ -337,80 +401,85 @@ async function launchDetached(opts: {
|
||||
signal,
|
||||
useDefaultEnvironment: true,
|
||||
onBundleFail: msg => {
|
||||
bundleFailMsg = msg;
|
||||
}
|
||||
});
|
||||
bundleFailMsg = msg
|
||||
},
|
||||
})
|
||||
if (!session) {
|
||||
logEvent('tengu_ultraplan_create_failed', {
|
||||
reason: (bundleFailMsg ? 'bundle_fail' : 'teleport_null') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
reason: (bundleFailMsg
|
||||
? 'bundle_fail'
|
||||
: 'teleport_null') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
enqueuePendingNotification({
|
||||
value: `ultraplan: session creation failed${bundleFailMsg ? ` — ${bundleFailMsg}` : ''}. See --debug for details.`,
|
||||
mode: 'task-notification'
|
||||
});
|
||||
return;
|
||||
mode: 'task-notification',
|
||||
})
|
||||
return
|
||||
}
|
||||
sessionId = session.id;
|
||||
const url = getRemoteSessionUrl(session.id, process.env.SESSION_INGRESS_URL);
|
||||
sessionId = session.id
|
||||
|
||||
const url = getRemoteSessionUrl(session.id, process.env.SESSION_INGRESS_URL)
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
ultraplanSessionUrl: url,
|
||||
ultraplanLaunching: undefined
|
||||
}));
|
||||
onSessionReady?.(buildSessionReadyMessage(url));
|
||||
ultraplanLaunching: undefined,
|
||||
}))
|
||||
onSessionReady?.(buildSessionReadyMessage(url))
|
||||
logEvent('tengu_ultraplan_launched', {
|
||||
has_seed_plan: Boolean(seedPlan),
|
||||
model: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
model:
|
||||
model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
// TODO(#23985): replace registerRemoteAgentTask + startDetachedPoll with
|
||||
// ExitPlanModeScanner inside startRemoteSessionPolling.
|
||||
const {
|
||||
taskId
|
||||
} = registerRemoteAgentTask({
|
||||
const { taskId } = registerRemoteAgentTask({
|
||||
remoteTaskType: 'ultraplan',
|
||||
session: {
|
||||
id: session.id,
|
||||
title: blurb || 'Ultraplan'
|
||||
},
|
||||
session: { id: session.id, title: blurb || 'Ultraplan' },
|
||||
command: blurb,
|
||||
context: {
|
||||
abortController: new AbortController(),
|
||||
getAppState,
|
||||
setAppState
|
||||
setAppState,
|
||||
},
|
||||
isUltraplan: true
|
||||
});
|
||||
startDetachedPoll(taskId, session.id, url, getAppState, setAppState);
|
||||
isUltraplan: true,
|
||||
})
|
||||
startDetachedPoll(taskId, session.id, url, getAppState, setAppState)
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
logError(e)
|
||||
logEvent('tengu_ultraplan_create_failed', {
|
||||
reason: 'unexpected_error' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
reason:
|
||||
'unexpected_error' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
enqueuePendingNotification({
|
||||
value: `ultraplan: unexpected error — ${errorMessage(e)}`,
|
||||
mode: 'task-notification'
|
||||
});
|
||||
mode: 'task-notification',
|
||||
})
|
||||
if (sessionId) {
|
||||
// Error after teleport succeeded — archive so the remote doesn't sit
|
||||
// running for 30min with nobody polling it.
|
||||
void archiveRemoteSession(sessionId).catch(err => logForDebugging('ultraplan: failed to archive orphaned session', err));
|
||||
void archiveRemoteSession(sessionId).catch(err =>
|
||||
logForDebugging('ultraplan: failed to archive orphaned session', err),
|
||||
)
|
||||
// ultraplanSessionUrl may have been set before the throw; clear it so
|
||||
// the "already polling" guard doesn't block future launches.
|
||||
setAppState(prev => prev.ultraplanSessionUrl ? {
|
||||
...prev,
|
||||
ultraplanSessionUrl: undefined
|
||||
} : prev);
|
||||
setAppState(prev =>
|
||||
prev.ultraplanSessionUrl
|
||||
? { ...prev, ultraplanSessionUrl: undefined }
|
||||
: prev,
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
// No-op on success: the url-setting setAppState already cleared this.
|
||||
setAppState(prev => prev.ultraplanLaunching ? {
|
||||
...prev,
|
||||
ultraplanLaunching: undefined
|
||||
} : prev);
|
||||
setAppState(prev =>
|
||||
prev.ultraplanLaunching
|
||||
? { ...prev, ultraplanLaunching: undefined }
|
||||
: prev,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const call: LocalJSXCommandCall = async (onDone, context, args) => {
|
||||
const blurb = args.trim();
|
||||
const blurb = args.trim()
|
||||
|
||||
// Bare /ultraplan (no args, no seed plan) just shows usage — no dialog.
|
||||
if (!blurb) {
|
||||
@@ -418,54 +487,42 @@ const call: LocalJSXCommandCall = async (onDone, context, args) => {
|
||||
blurb,
|
||||
getAppState: context.getAppState,
|
||||
setAppState: context.setAppState,
|
||||
signal: context.abortController.signal
|
||||
});
|
||||
onDone(msg, {
|
||||
display: 'system'
|
||||
});
|
||||
return null;
|
||||
signal: context.abortController.signal,
|
||||
})
|
||||
onDone(msg, { display: 'system' })
|
||||
return null
|
||||
}
|
||||
|
||||
// Guard matches launchUltraplan's own check — showing the dialog when a
|
||||
// session is already active or launching would waste the user's click and set
|
||||
// hasSeenUltraplanTerms before the launch fails.
|
||||
const {
|
||||
ultraplanSessionUrl: active,
|
||||
ultraplanLaunching
|
||||
} = context.getAppState();
|
||||
const { ultraplanSessionUrl: active, ultraplanLaunching } =
|
||||
context.getAppState()
|
||||
if (active || ultraplanLaunching) {
|
||||
logEvent('tengu_ultraplan_create_failed', {
|
||||
reason: (active ? 'already_polling' : 'already_launching') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
onDone(buildAlreadyActiveMessage(active), {
|
||||
display: 'system'
|
||||
});
|
||||
return null;
|
||||
reason: (active
|
||||
? 'already_polling'
|
||||
: 'already_launching') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
onDone(buildAlreadyActiveMessage(active), { display: 'system' })
|
||||
return null
|
||||
}
|
||||
|
||||
// Mount the pre-launch dialog via focusedInputDialog (bottom region, like
|
||||
// permission dialogs) rather than returning JSX (transcript area, anchors
|
||||
// at top of scrollback). REPL.tsx handles launch/clear/cancel on choice.
|
||||
context.setAppState(prev => ({
|
||||
...prev,
|
||||
ultraplanLaunchPending: {
|
||||
blurb
|
||||
}
|
||||
}));
|
||||
context.setAppState(prev => ({ ...prev, ultraplanLaunchPending: { blurb } }))
|
||||
// 'skip' suppresses the (no content) echo — the dialog's choice handler
|
||||
// adds the real /ultraplan echo + launch confirmation.
|
||||
onDone(undefined, {
|
||||
display: 'skip'
|
||||
});
|
||||
return null;
|
||||
};
|
||||
onDone(undefined, { display: 'skip' })
|
||||
return null
|
||||
}
|
||||
|
||||
export default {
|
||||
type: 'local-jsx',
|
||||
name: 'ultraplan',
|
||||
description: `~10–30 min · Claude Code on the web drafts an advanced plan you can edit and approve. See ${CCR_TERMS_URL}`,
|
||||
argumentHint: '<prompt>',
|
||||
isEnabled: () => (process.env.USER_TYPE) === 'ant',
|
||||
load: () => Promise.resolve({
|
||||
call
|
||||
})
|
||||
} satisfies Command;
|
||||
isEnabled: () => process.env.USER_TYPE === 'ant',
|
||||
load: () => Promise.resolve({ call }),
|
||||
} satisfies Command
|
||||
|
||||
@@ -1,37 +1,67 @@
|
||||
import * as React from 'react';
|
||||
import type { LocalJSXCommandContext } from '../../commands.js';
|
||||
import { getOauthProfileFromOauthToken } from '../../services/oauth/getOauthProfile.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { getClaudeAIOAuthTokens, isClaudeAISubscriber } from '../../utils/auth.js';
|
||||
import { openBrowser } from '../../utils/browser.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import { Login } from '../login/login.js';
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext): Promise<React.ReactNode | null> {
|
||||
import * as React from 'react'
|
||||
import type { LocalJSXCommandContext } from '../../commands.js'
|
||||
import { getOauthProfileFromOauthToken } from '../../services/oauth/getOauthProfile.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import {
|
||||
getClaudeAIOAuthTokens,
|
||||
isClaudeAISubscriber,
|
||||
} from '../../utils/auth.js'
|
||||
import { openBrowser } from '../../utils/browser.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import { Login } from '../login/login.js'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
): Promise<React.ReactNode | null> {
|
||||
try {
|
||||
// Check if user is already on the highest Max plan (20x)
|
||||
if (isClaudeAISubscriber()) {
|
||||
const tokens = getClaudeAIOAuthTokens();
|
||||
let isMax20x = false;
|
||||
const tokens = getClaudeAIOAuthTokens()
|
||||
let isMax20x = false
|
||||
|
||||
if (tokens?.subscriptionType && tokens?.rateLimitTier) {
|
||||
isMax20x = tokens.subscriptionType === 'max' && tokens.rateLimitTier === 'default_claude_max_20x';
|
||||
isMax20x =
|
||||
tokens.subscriptionType === 'max' &&
|
||||
tokens.rateLimitTier === 'default_claude_max_20x'
|
||||
} else if (tokens?.accessToken) {
|
||||
const profile = await getOauthProfileFromOauthToken(tokens.accessToken);
|
||||
isMax20x = profile?.organization?.organization_type === 'claude_max' && profile?.organization?.rate_limit_tier === 'default_claude_max_20x';
|
||||
const profile = await getOauthProfileFromOauthToken(tokens.accessToken)
|
||||
isMax20x =
|
||||
profile?.organization?.organization_type === 'claude_max' &&
|
||||
profile?.organization?.rate_limit_tier === 'default_claude_max_20x'
|
||||
}
|
||||
|
||||
if (isMax20x) {
|
||||
setTimeout(onDone, 0, 'You are already on the highest Max subscription plan. For additional usage, run /login to switch to an API usage-billed account.');
|
||||
return null;
|
||||
setTimeout(
|
||||
onDone,
|
||||
0,
|
||||
'You are already on the highest Max subscription plan. For additional usage, run /login to switch to an API usage-billed account.',
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
const url = 'https://claude.ai/upgrade/max';
|
||||
await openBrowser(url);
|
||||
return <Login startingMessage={'Starting new login following /upgrade. Exit with Ctrl-C to use existing account.'} onDone={success => {
|
||||
context.onChangeAPIKey();
|
||||
onDone(success ? 'Login successful' : 'Login interrupted');
|
||||
}} />;
|
||||
|
||||
const url = 'https://claude.ai/upgrade/max'
|
||||
await openBrowser(url)
|
||||
|
||||
return (
|
||||
<Login
|
||||
startingMessage={
|
||||
'Starting new login following /upgrade. Exit with Ctrl-C to use existing account.'
|
||||
}
|
||||
onDone={success => {
|
||||
context.onChangeAPIKey()
|
||||
onDone(success ? 'Login successful' : 'Login interrupted')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
} catch (error) {
|
||||
logError(error as Error);
|
||||
setTimeout(onDone, 0, 'Failed to open browser. Please visit https://claude.ai/upgrade/max to upgrade.');
|
||||
logError(error as Error)
|
||||
setTimeout(
|
||||
onDone,
|
||||
0,
|
||||
'Failed to open browser. Please visit https://claude.ai/upgrade/max to upgrade.',
|
||||
)
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { Settings } from '../../components/Settings/Settings.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import * as React from 'react'
|
||||
import { Settings } from '../../components/Settings/Settings.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, context) => {
|
||||
return <Settings onClose={onDone} context={context} defaultTab="Usage" />;
|
||||
};
|
||||
return <Settings onClose={onDone} context={context} defaultTab="Usage" />
|
||||
}
|
||||
|
||||
@@ -1,135 +1,105 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from '../ink.js';
|
||||
import { formatNumber } from '../utils/format.js';
|
||||
import type { Theme } from '../utils/theme.js';
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from '../ink.js'
|
||||
import { formatNumber } from '../utils/format.js'
|
||||
import type { Theme } from '../utils/theme.js'
|
||||
|
||||
type Props = {
|
||||
agentType: string;
|
||||
description?: string;
|
||||
name?: string;
|
||||
descriptionColor?: keyof Theme;
|
||||
taskDescription?: string;
|
||||
toolUseCount: number;
|
||||
tokens: number | null;
|
||||
color?: keyof Theme;
|
||||
isLast: boolean;
|
||||
isResolved: boolean;
|
||||
isError: boolean;
|
||||
isAsync?: boolean;
|
||||
shouldAnimate: boolean;
|
||||
lastToolInfo?: string | null;
|
||||
hideType?: boolean;
|
||||
};
|
||||
export function AgentProgressLine(t0) {
|
||||
const $ = _c(32);
|
||||
const {
|
||||
agentType,
|
||||
description,
|
||||
name,
|
||||
descriptionColor,
|
||||
taskDescription,
|
||||
toolUseCount,
|
||||
tokens,
|
||||
color,
|
||||
isLast,
|
||||
isResolved,
|
||||
isAsync: t1,
|
||||
lastToolInfo,
|
||||
hideType: t2
|
||||
} = t0;
|
||||
const isAsync = t1 === undefined ? false : t1;
|
||||
const hideType = t2 === undefined ? false : t2;
|
||||
const treeChar = isLast ? "\u2514\u2500" : "\u251C\u2500";
|
||||
const isBackgrounded = isAsync && isResolved;
|
||||
let t3;
|
||||
if ($[0] !== isBackgrounded || $[1] !== isResolved || $[2] !== lastToolInfo || $[3] !== taskDescription) {
|
||||
t3 = () => {
|
||||
if (!isResolved) {
|
||||
return lastToolInfo || "Initializing\u2026";
|
||||
}
|
||||
if (isBackgrounded) {
|
||||
return taskDescription ?? "Running in the background";
|
||||
}
|
||||
return "Done";
|
||||
};
|
||||
$[0] = isBackgrounded;
|
||||
$[1] = isResolved;
|
||||
$[2] = lastToolInfo;
|
||||
$[3] = taskDescription;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
const getStatusText = t3;
|
||||
let t4;
|
||||
if ($[5] !== treeChar) {
|
||||
t4 = <Text dimColor={true}>{treeChar} </Text>;
|
||||
$[5] = treeChar;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
const t5 = !isResolved;
|
||||
let t6;
|
||||
if ($[7] !== agentType || $[8] !== color || $[9] !== description || $[10] !== descriptionColor || $[11] !== hideType || $[12] !== name) {
|
||||
t6 = hideType ? <><Text bold={true}>{name ?? description ?? agentType}</Text>{name && description && <Text dimColor={true}>: {description}</Text>}</> : <><Text bold={true} backgroundColor={color} color={color ? "inverseText" : undefined}>{agentType}</Text>{description && <>{" ("}<Text backgroundColor={descriptionColor} color={descriptionColor ? "inverseText" : undefined}>{description}</Text>{")"}</>}</>;
|
||||
$[7] = agentType;
|
||||
$[8] = color;
|
||||
$[9] = description;
|
||||
$[10] = descriptionColor;
|
||||
$[11] = hideType;
|
||||
$[12] = name;
|
||||
$[13] = t6;
|
||||
} else {
|
||||
t6 = $[13];
|
||||
}
|
||||
let t7;
|
||||
if ($[14] !== isBackgrounded || $[15] !== tokens || $[16] !== toolUseCount) {
|
||||
t7 = !isBackgrounded && <>{" \xB7 "}{toolUseCount} tool {toolUseCount === 1 ? "use" : "uses"}{tokens !== null && <> · {formatNumber(tokens)} tokens</>}</>;
|
||||
$[14] = isBackgrounded;
|
||||
$[15] = tokens;
|
||||
$[16] = toolUseCount;
|
||||
$[17] = t7;
|
||||
} else {
|
||||
t7 = $[17];
|
||||
}
|
||||
let t8;
|
||||
if ($[18] !== t5 || $[19] !== t6 || $[20] !== t7) {
|
||||
t8 = <Text dimColor={t5}>{t6}{t7}</Text>;
|
||||
$[18] = t5;
|
||||
$[19] = t6;
|
||||
$[20] = t7;
|
||||
$[21] = t8;
|
||||
} else {
|
||||
t8 = $[21];
|
||||
}
|
||||
let t9;
|
||||
if ($[22] !== t4 || $[23] !== t8) {
|
||||
t9 = <Box paddingLeft={3}>{t4}{t8}</Box>;
|
||||
$[22] = t4;
|
||||
$[23] = t8;
|
||||
$[24] = t9;
|
||||
} else {
|
||||
t9 = $[24];
|
||||
}
|
||||
let t10;
|
||||
if ($[25] !== getStatusText || $[26] !== isBackgrounded || $[27] !== isLast) {
|
||||
t10 = !isBackgrounded && <Box paddingLeft={3} flexDirection="row"><Text dimColor={true}>{isLast ? " \u23BF " : "\u2502 \u23BF "}</Text><Text dimColor={true}>{getStatusText()}</Text></Box>;
|
||||
$[25] = getStatusText;
|
||||
$[26] = isBackgrounded;
|
||||
$[27] = isLast;
|
||||
$[28] = t10;
|
||||
} else {
|
||||
t10 = $[28];
|
||||
}
|
||||
let t11;
|
||||
if ($[29] !== t10 || $[30] !== t9) {
|
||||
t11 = <Box flexDirection="column">{t9}{t10}</Box>;
|
||||
$[29] = t10;
|
||||
$[30] = t9;
|
||||
$[31] = t11;
|
||||
} else {
|
||||
t11 = $[31];
|
||||
}
|
||||
return t11;
|
||||
agentType: string
|
||||
description?: string
|
||||
name?: string
|
||||
descriptionColor?: keyof Theme
|
||||
taskDescription?: string
|
||||
toolUseCount: number
|
||||
tokens: number | null
|
||||
color?: keyof Theme
|
||||
isLast: boolean
|
||||
isResolved: boolean
|
||||
isError: boolean
|
||||
isAsync?: boolean
|
||||
shouldAnimate: boolean
|
||||
lastToolInfo?: string | null
|
||||
hideType?: boolean
|
||||
}
|
||||
|
||||
export function AgentProgressLine({
|
||||
agentType,
|
||||
description,
|
||||
name,
|
||||
descriptionColor,
|
||||
taskDescription,
|
||||
toolUseCount,
|
||||
tokens,
|
||||
color,
|
||||
isLast,
|
||||
isResolved,
|
||||
isError: _isError,
|
||||
isAsync = false,
|
||||
shouldAnimate: _shouldAnimate,
|
||||
lastToolInfo,
|
||||
hideType = false,
|
||||
}: Props): React.ReactNode {
|
||||
const treeChar = isLast ? '└─' : '├─'
|
||||
const isBackgrounded = isAsync && isResolved
|
||||
|
||||
// Determine the status text
|
||||
const getStatusText = (): string => {
|
||||
if (!isResolved) {
|
||||
return lastToolInfo || 'Initializing…'
|
||||
}
|
||||
if (isBackgrounded) {
|
||||
return taskDescription ?? 'Running in the background'
|
||||
}
|
||||
return 'Done'
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box paddingLeft={3}>
|
||||
<Text dimColor>{treeChar} </Text>
|
||||
<Text dimColor={!isResolved}>
|
||||
{hideType ? (
|
||||
<>
|
||||
<Text bold>{name ?? description ?? agentType}</Text>
|
||||
{name && description && <Text dimColor>: {description}</Text>}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text
|
||||
bold
|
||||
backgroundColor={color}
|
||||
color={color ? 'inverseText' : undefined}
|
||||
>
|
||||
{agentType}
|
||||
</Text>
|
||||
{description && (
|
||||
<>
|
||||
{' ('}
|
||||
<Text
|
||||
backgroundColor={descriptionColor}
|
||||
color={descriptionColor ? 'inverseText' : undefined}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
{')'}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!isBackgrounded && (
|
||||
<>
|
||||
{' · '}
|
||||
{toolUseCount} tool {toolUseCount === 1 ? 'use' : 'uses'}
|
||||
{tokens !== null && <> · {formatNumber(tokens)} tokens</>}
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
{!isBackgrounded && (
|
||||
<Box paddingLeft={3} flexDirection="row">
|
||||
<Text dimColor>{isLast ? ' ⎿ ' : '│ ⎿ '}</Text>
|
||||
<Text dimColor>{getStatusText()}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,55 +1,37 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { FpsMetricsProvider } from '../context/fpsMetrics.js';
|
||||
import { StatsProvider, type StatsStore } from '../context/stats.js';
|
||||
import { type AppState, AppStateProvider } from '../state/AppState.js';
|
||||
import { onChangeAppState } from '../state/onChangeAppState.js';
|
||||
import type { FpsMetrics } from '../utils/fpsTracker.js';
|
||||
import React from 'react'
|
||||
import { FpsMetricsProvider } from '../context/fpsMetrics.js'
|
||||
import { StatsProvider, type StatsStore } from '../context/stats.js'
|
||||
import { type AppState, AppStateProvider } from '../state/AppState.js'
|
||||
import { onChangeAppState } from '../state/onChangeAppState.js'
|
||||
import type { FpsMetrics } from '../utils/fpsTracker.js'
|
||||
|
||||
type Props = {
|
||||
getFpsMetrics: () => FpsMetrics | undefined;
|
||||
stats?: StatsStore;
|
||||
initialState: AppState;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
getFpsMetrics: () => FpsMetrics | undefined
|
||||
stats?: StatsStore
|
||||
initialState: AppState
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Top-level wrapper for interactive sessions.
|
||||
* Provides FPS metrics, stats context, and app state to the component tree.
|
||||
*/
|
||||
export function App(t0) {
|
||||
const $ = _c(9);
|
||||
const {
|
||||
getFpsMetrics,
|
||||
stats,
|
||||
initialState,
|
||||
children
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== children || $[1] !== initialState) {
|
||||
t1 = <AppStateProvider initialState={initialState} onChangeAppState={onChangeAppState}>{children}</AppStateProvider>;
|
||||
$[0] = children;
|
||||
$[1] = initialState;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== stats || $[4] !== t1) {
|
||||
t2 = <StatsProvider store={stats}>{t1}</StatsProvider>;
|
||||
$[3] = stats;
|
||||
$[4] = t1;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
let t3;
|
||||
if ($[6] !== getFpsMetrics || $[7] !== t2) {
|
||||
t3 = <FpsMetricsProvider getFpsMetrics={getFpsMetrics}>{t2}</FpsMetricsProvider>;
|
||||
$[6] = getFpsMetrics;
|
||||
$[7] = t2;
|
||||
$[8] = t3;
|
||||
} else {
|
||||
t3 = $[8];
|
||||
}
|
||||
return t3;
|
||||
export function App({
|
||||
getFpsMetrics,
|
||||
stats,
|
||||
initialState,
|
||||
children,
|
||||
}: Props): React.ReactNode {
|
||||
return (
|
||||
<FpsMetricsProvider getFpsMetrics={getFpsMetrics}>
|
||||
<StatsProvider store={stats}>
|
||||
<AppStateProvider
|
||||
initialState={initialState}
|
||||
onChangeAppState={onChangeAppState}
|
||||
>
|
||||
{children}
|
||||
</AppStateProvider>
|
||||
</StatsProvider>
|
||||
</FpsMetricsProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,122 +1,79 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Text } from '../ink.js';
|
||||
import { saveGlobalConfig } from '../utils/config.js';
|
||||
import { Select } from './CustomSelect/index.js';
|
||||
import { Dialog } from './design-system/Dialog.js';
|
||||
import React from 'react'
|
||||
import { Text } from '../ink.js'
|
||||
import { saveGlobalConfig } from '../utils/config.js'
|
||||
import { Select } from './CustomSelect/index.js'
|
||||
import { Dialog } from './design-system/Dialog.js'
|
||||
|
||||
type Props = {
|
||||
customApiKeyTruncated: string;
|
||||
onDone(approved: boolean): void;
|
||||
};
|
||||
export function ApproveApiKey(t0) {
|
||||
const $ = _c(17);
|
||||
const {
|
||||
customApiKeyTruncated,
|
||||
onDone
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== customApiKeyTruncated || $[1] !== onDone) {
|
||||
t1 = function onChange(value) {
|
||||
bb2: switch (value) {
|
||||
case "yes":
|
||||
{
|
||||
saveGlobalConfig(current_0 => ({
|
||||
...current_0,
|
||||
customApiKeyResponses: {
|
||||
...current_0.customApiKeyResponses,
|
||||
approved: [...(current_0.customApiKeyResponses?.approved ?? []), customApiKeyTruncated]
|
||||
}
|
||||
}));
|
||||
onDone(true);
|
||||
break bb2;
|
||||
}
|
||||
case "no":
|
||||
{
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
customApiKeyResponses: {
|
||||
...current.customApiKeyResponses,
|
||||
rejected: [...(current.customApiKeyResponses?.rejected ?? []), customApiKeyTruncated]
|
||||
}
|
||||
}));
|
||||
onDone(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
$[0] = customApiKeyTruncated;
|
||||
$[1] = onDone;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
const onChange = t1;
|
||||
let t2;
|
||||
if ($[3] !== onChange) {
|
||||
t2 = () => onChange("no");
|
||||
$[3] = onChange;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
let t3;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Text bold={true}>ANTHROPIC_API_KEY</Text>;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
let t4;
|
||||
if ($[6] !== customApiKeyTruncated) {
|
||||
t4 = <Text>{t3}<Text>: sk-ant-...{customApiKeyTruncated}</Text></Text>;
|
||||
$[6] = customApiKeyTruncated;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
let t5;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Text>Do you want to use this API key?</Text>;
|
||||
$[8] = t5;
|
||||
} else {
|
||||
t5 = $[8];
|
||||
}
|
||||
let t6;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = {
|
||||
label: "Yes",
|
||||
value: "yes"
|
||||
};
|
||||
$[9] = t6;
|
||||
} else {
|
||||
t6 = $[9];
|
||||
}
|
||||
let t7;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = [t6, {
|
||||
label: <Text>No (<Text bold={true}>recommended</Text>)</Text>,
|
||||
value: "no"
|
||||
}];
|
||||
$[10] = t7;
|
||||
} else {
|
||||
t7 = $[10];
|
||||
}
|
||||
let t8;
|
||||
if ($[11] !== onChange) {
|
||||
t8 = <Select defaultValue="no" defaultFocusValue="no" options={t7} onChange={value_0 => onChange(value_0 as 'yes' | 'no')} onCancel={() => onChange("no")} />;
|
||||
$[11] = onChange;
|
||||
$[12] = t8;
|
||||
} else {
|
||||
t8 = $[12];
|
||||
}
|
||||
let t9;
|
||||
if ($[13] !== t2 || $[14] !== t4 || $[15] !== t8) {
|
||||
t9 = <Dialog title="Detected a custom API key in your environment" color="warning" onCancel={t2}>{t4}{t5}{t8}</Dialog>;
|
||||
$[13] = t2;
|
||||
$[14] = t4;
|
||||
$[15] = t8;
|
||||
$[16] = t9;
|
||||
} else {
|
||||
t9 = $[16];
|
||||
}
|
||||
return t9;
|
||||
customApiKeyTruncated: string
|
||||
onDone(approved: boolean): void
|
||||
}
|
||||
|
||||
export function ApproveApiKey({
|
||||
customApiKeyTruncated,
|
||||
onDone,
|
||||
}: Props): React.ReactNode {
|
||||
function onChange(value: 'yes' | 'no') {
|
||||
switch (value) {
|
||||
case 'yes': {
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
customApiKeyResponses: {
|
||||
...current.customApiKeyResponses,
|
||||
approved: [
|
||||
...(current.customApiKeyResponses?.approved ?? []),
|
||||
customApiKeyTruncated,
|
||||
],
|
||||
},
|
||||
}))
|
||||
onDone(true)
|
||||
break
|
||||
}
|
||||
case 'no': {
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
customApiKeyResponses: {
|
||||
...current.customApiKeyResponses,
|
||||
rejected: [
|
||||
...(current.customApiKeyResponses?.rejected ?? []),
|
||||
customApiKeyTruncated,
|
||||
],
|
||||
},
|
||||
}))
|
||||
onDone(false)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="Detected a custom API key in your environment"
|
||||
color="warning"
|
||||
onCancel={() => onChange('no')}
|
||||
>
|
||||
<Text>
|
||||
<Text bold>ANTHROPIC_API_KEY</Text>
|
||||
<Text>: sk-ant-...{customApiKeyTruncated}</Text>
|
||||
</Text>
|
||||
<Text>Do you want to use this API key?</Text>
|
||||
<Select
|
||||
defaultValue="no"
|
||||
defaultFocusValue="no"
|
||||
options={[
|
||||
{ label: 'Yes', value: 'yes' },
|
||||
{
|
||||
label: (
|
||||
<Text>
|
||||
No (<Text bold>recommended</Text>)
|
||||
</Text>
|
||||
),
|
||||
value: 'no',
|
||||
},
|
||||
]}
|
||||
onChange={value => onChange(value as 'yes' | 'no')}
|
||||
onCancel={() => onChange('no')}
|
||||
/>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,141 +1,86 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { logEvent } from 'src/services/analytics/index.js';
|
||||
import { Box, Link, Text } from '../ink.js';
|
||||
import { updateSettingsForSource } from '../utils/settings/settings.js';
|
||||
import { Select } from './CustomSelect/index.js';
|
||||
import { Dialog } from './design-system/Dialog.js';
|
||||
import React from 'react'
|
||||
import { logEvent } from 'src/services/analytics/index.js'
|
||||
import { Box, Link, Text } from '../ink.js'
|
||||
import { updateSettingsForSource } from '../utils/settings/settings.js'
|
||||
import { Select } from './CustomSelect/index.js'
|
||||
import { Dialog } from './design-system/Dialog.js'
|
||||
|
||||
// NOTE: This copy is legally reviewed — do not modify without Legal team approval.
|
||||
export const AUTO_MODE_DESCRIPTION = "Auto mode lets Claude handle permission prompts automatically — Claude checks each tool call for risky actions and prompt injection before executing. Actions Claude identifies as safe are executed, while actions Claude identifies as risky are blocked and Claude may try a different approach. Ideal for long-running tasks. Sessions are slightly more expensive. Claude can make mistakes that allow harmful commands to run, it's recommended to only use in isolated environments. Shift+Tab to change mode.";
|
||||
export const AUTO_MODE_DESCRIPTION =
|
||||
"Auto mode lets Claude handle permission prompts automatically — Claude checks each tool call for risky actions and prompt injection before executing. Actions Claude identifies as safe are executed, while actions Claude identifies as risky are blocked and Claude may try a different approach. Ideal for long-running tasks. Sessions are slightly more expensive. Claude can make mistakes that allow harmful commands to run, it's recommended to only use in isolated environments. Shift+Tab to change mode."
|
||||
|
||||
type Props = {
|
||||
onAccept(): void;
|
||||
onDecline(): void;
|
||||
onAccept(): void
|
||||
onDecline(): void
|
||||
// Startup gate: decline exits the process, so relabel accordingly.
|
||||
declineExits?: boolean;
|
||||
};
|
||||
export function AutoModeOptInDialog(t0) {
|
||||
const $ = _c(18);
|
||||
const {
|
||||
onAccept,
|
||||
onDecline,
|
||||
declineExits
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = [];
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
React.useEffect(_temp, t1);
|
||||
let t2;
|
||||
if ($[1] !== onAccept || $[2] !== onDecline) {
|
||||
t2 = function onChange(value) {
|
||||
bb3: switch (value) {
|
||||
case "accept":
|
||||
{
|
||||
logEvent("tengu_auto_mode_opt_in_dialog_accept", {});
|
||||
updateSettingsForSource("userSettings", {
|
||||
skipAutoPermissionPrompt: true
|
||||
});
|
||||
onAccept();
|
||||
break bb3;
|
||||
}
|
||||
case "accept-default":
|
||||
{
|
||||
logEvent("tengu_auto_mode_opt_in_dialog_accept_default", {});
|
||||
updateSettingsForSource("userSettings", {
|
||||
skipAutoPermissionPrompt: true,
|
||||
permissions: {
|
||||
defaultMode: "auto"
|
||||
}
|
||||
});
|
||||
onAccept();
|
||||
break bb3;
|
||||
}
|
||||
case "decline":
|
||||
{
|
||||
logEvent("tengu_auto_mode_opt_in_dialog_decline", {});
|
||||
onDecline();
|
||||
}
|
||||
declineExits?: boolean
|
||||
}
|
||||
|
||||
export function AutoModeOptInDialog({
|
||||
onAccept,
|
||||
onDecline,
|
||||
declineExits,
|
||||
}: Props): React.ReactNode {
|
||||
React.useEffect(() => {
|
||||
logEvent('tengu_auto_mode_opt_in_dialog_shown', {})
|
||||
}, [])
|
||||
|
||||
function onChange(value: 'accept' | 'accept-default' | 'decline') {
|
||||
switch (value) {
|
||||
case 'accept': {
|
||||
logEvent('tengu_auto_mode_opt_in_dialog_accept', {})
|
||||
updateSettingsForSource('userSettings', {
|
||||
skipAutoPermissionPrompt: true,
|
||||
})
|
||||
onAccept()
|
||||
break
|
||||
}
|
||||
};
|
||||
$[1] = onAccept;
|
||||
$[2] = onDecline;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
case 'accept-default': {
|
||||
logEvent('tengu_auto_mode_opt_in_dialog_accept_default', {})
|
||||
updateSettingsForSource('userSettings', {
|
||||
skipAutoPermissionPrompt: true,
|
||||
permissions: { defaultMode: 'auto' },
|
||||
})
|
||||
onAccept()
|
||||
break
|
||||
}
|
||||
case 'decline': {
|
||||
logEvent('tengu_auto_mode_opt_in_dialog_decline', {})
|
||||
onDecline()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
const onChange = t2;
|
||||
let t3;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Box flexDirection="column" gap={1}><Text>{AUTO_MODE_DESCRIPTION}</Text><Link url="https://code.claude.com/docs/en/security" /></Box>;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = true ? [{
|
||||
label: "Yes, and make it my default mode",
|
||||
value: "accept-default" as const
|
||||
}] : [];
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
let t5;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = {
|
||||
label: "Yes, enable auto mode",
|
||||
value: "accept" as const
|
||||
};
|
||||
$[6] = t5;
|
||||
} else {
|
||||
t5 = $[6];
|
||||
}
|
||||
const t6 = declineExits ? "No, exit" : "No, go back";
|
||||
let t7;
|
||||
if ($[7] !== t6) {
|
||||
t7 = [...t4, t5, {
|
||||
label: t6,
|
||||
value: "decline" as const
|
||||
}];
|
||||
$[7] = t6;
|
||||
$[8] = t7;
|
||||
} else {
|
||||
t7 = $[8];
|
||||
}
|
||||
let t8;
|
||||
if ($[9] !== onChange) {
|
||||
t8 = value_0 => onChange(value_0 as 'accept' | 'accept-default' | 'decline');
|
||||
$[9] = onChange;
|
||||
$[10] = t8;
|
||||
} else {
|
||||
t8 = $[10];
|
||||
}
|
||||
let t9;
|
||||
if ($[11] !== onDecline || $[12] !== t7 || $[13] !== t8) {
|
||||
t9 = <Select options={t7} onChange={t8} onCancel={onDecline} />;
|
||||
$[11] = onDecline;
|
||||
$[12] = t7;
|
||||
$[13] = t8;
|
||||
$[14] = t9;
|
||||
} else {
|
||||
t9 = $[14];
|
||||
}
|
||||
let t10;
|
||||
if ($[15] !== onDecline || $[16] !== t9) {
|
||||
t10 = <Dialog title="Enable auto mode?" color="warning" onCancel={onDecline}>{t3}{t9}</Dialog>;
|
||||
$[15] = onDecline;
|
||||
$[16] = t9;
|
||||
$[17] = t10;
|
||||
} else {
|
||||
t10 = $[17];
|
||||
}
|
||||
return t10;
|
||||
}
|
||||
function _temp() {
|
||||
logEvent("tengu_auto_mode_opt_in_dialog_shown", {});
|
||||
|
||||
return (
|
||||
<Dialog title="Enable auto mode?" color="warning" onCancel={onDecline}>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text>{AUTO_MODE_DESCRIPTION}</Text>
|
||||
|
||||
<Link url="https://code.claude.com/docs/en/security" />
|
||||
</Box>
|
||||
|
||||
<Select
|
||||
options={[
|
||||
...("external" !== 'ant'
|
||||
? [
|
||||
{
|
||||
label: 'Yes, and make it my default mode',
|
||||
value: 'accept-default' as const,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ label: 'Yes, enable auto mode', value: 'accept' as const },
|
||||
{
|
||||
label: declineExits ? 'No, exit' : 'No, go back',
|
||||
value: 'decline' as const,
|
||||
},
|
||||
]}
|
||||
onChange={value =>
|
||||
onChange(value as 'accept' | 'accept-default' | 'decline')
|
||||
}
|
||||
onCancel={onDecline}
|
||||
/>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,197 +1,264 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
|
||||
import { useInterval } from 'usehooks-ts';
|
||||
import { useUpdateNotification } from '../hooks/useUpdateNotification.js';
|
||||
import { Box, Text } from '../ink.js';
|
||||
import { type AutoUpdaterResult, getLatestVersion, getMaxVersion, type InstallStatus, installGlobalPackage, shouldSkipVersion } from '../utils/autoUpdater.js';
|
||||
import { getGlobalConfig, isAutoUpdaterDisabled } from '../utils/config.js';
|
||||
import { logForDebugging } from '../utils/debug.js';
|
||||
import { getCurrentInstallationType } from '../utils/doctorDiagnostic.js';
|
||||
import { installOrUpdateClaudePackage, localInstallationExists } from '../utils/localInstaller.js';
|
||||
import { removeInstalledSymlink } from '../utils/nativeInstaller/index.js';
|
||||
import { gt, gte } from '../utils/semver.js';
|
||||
import { getInitialSettings } from '../utils/settings/settings.js';
|
||||
import * as React from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from 'src/services/analytics/index.js'
|
||||
import { useInterval } from 'usehooks-ts'
|
||||
import { useUpdateNotification } from '../hooks/useUpdateNotification.js'
|
||||
import { Box, Text } from '../ink.js'
|
||||
import {
|
||||
type AutoUpdaterResult,
|
||||
getLatestVersion,
|
||||
getMaxVersion,
|
||||
type InstallStatus,
|
||||
installGlobalPackage,
|
||||
shouldSkipVersion,
|
||||
} from '../utils/autoUpdater.js'
|
||||
import { getGlobalConfig, isAutoUpdaterDisabled } from '../utils/config.js'
|
||||
import { logForDebugging } from '../utils/debug.js'
|
||||
import { getCurrentInstallationType } from '../utils/doctorDiagnostic.js'
|
||||
import {
|
||||
installOrUpdateClaudePackage,
|
||||
localInstallationExists,
|
||||
} from '../utils/localInstaller.js'
|
||||
import { removeInstalledSymlink } from '../utils/nativeInstaller/index.js'
|
||||
import { gt, gte } from '../utils/semver.js'
|
||||
import { getInitialSettings } from '../utils/settings/settings.js'
|
||||
|
||||
type Props = {
|
||||
isUpdating: boolean;
|
||||
onChangeIsUpdating: (isUpdating: boolean) => void;
|
||||
onAutoUpdaterResult: (autoUpdaterResult: AutoUpdaterResult) => void;
|
||||
autoUpdaterResult: AutoUpdaterResult | null;
|
||||
showSuccessMessage: boolean;
|
||||
verbose: boolean;
|
||||
};
|
||||
isUpdating: boolean
|
||||
onChangeIsUpdating: (isUpdating: boolean) => void
|
||||
onAutoUpdaterResult: (autoUpdaterResult: AutoUpdaterResult) => void
|
||||
autoUpdaterResult: AutoUpdaterResult | null
|
||||
showSuccessMessage: boolean
|
||||
verbose: boolean
|
||||
}
|
||||
|
||||
export function AutoUpdater({
|
||||
isUpdating,
|
||||
onChangeIsUpdating,
|
||||
onAutoUpdaterResult,
|
||||
autoUpdaterResult,
|
||||
showSuccessMessage,
|
||||
verbose
|
||||
verbose,
|
||||
}: Props): React.ReactNode {
|
||||
const [versions, setVersions] = useState<{
|
||||
global?: string | null;
|
||||
latest?: string | null;
|
||||
}>({});
|
||||
const [hasLocalInstall, setHasLocalInstall] = useState(false);
|
||||
const updateSemver = useUpdateNotification(autoUpdaterResult?.version);
|
||||
global?: string | null
|
||||
latest?: string | null
|
||||
}>({})
|
||||
const [hasLocalInstall, setHasLocalInstall] = useState(false)
|
||||
const updateSemver = useUpdateNotification(autoUpdaterResult?.version)
|
||||
|
||||
useEffect(() => {
|
||||
void localInstallationExists().then(setHasLocalInstall);
|
||||
}, []);
|
||||
void localInstallationExists().then(setHasLocalInstall)
|
||||
}, [])
|
||||
|
||||
// Track latest isUpdating value in a ref so the memoized checkForUpdates
|
||||
// callback always sees the current value. Without this, the 30-minute
|
||||
// interval fires with a stale closure where isUpdating is false, allowing
|
||||
// a concurrent installGlobalPackage() to run while one is already in
|
||||
// progress.
|
||||
const isUpdatingRef = useRef(isUpdating);
|
||||
isUpdatingRef.current = isUpdating;
|
||||
const isUpdatingRef = useRef(isUpdating)
|
||||
isUpdatingRef.current = isUpdating
|
||||
|
||||
const checkForUpdates = React.useCallback(async () => {
|
||||
if (isUpdatingRef.current) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (("production" as string) === 'test' || ("production" as string) === 'development') {
|
||||
logForDebugging('AutoUpdater: Skipping update check in test/dev environment');
|
||||
return;
|
||||
|
||||
if (
|
||||
"production" === 'test' ||
|
||||
"production" === 'development'
|
||||
) {
|
||||
logForDebugging(
|
||||
'AutoUpdater: Skipping update check in test/dev environment',
|
||||
)
|
||||
return
|
||||
}
|
||||
const currentVersion = MACRO.VERSION;
|
||||
const channel = getInitialSettings()?.autoUpdatesChannel ?? 'latest';
|
||||
let latestVersion = await getLatestVersion(channel);
|
||||
const isDisabled = isAutoUpdaterDisabled();
|
||||
|
||||
const currentVersion = MACRO.VERSION
|
||||
const channel = getInitialSettings()?.autoUpdatesChannel ?? 'latest'
|
||||
let latestVersion = await getLatestVersion(channel)
|
||||
const isDisabled = isAutoUpdaterDisabled()
|
||||
|
||||
// Check if max version is set (server-side kill switch for auto-updates)
|
||||
const maxVersion = await getMaxVersion();
|
||||
const maxVersion = await getMaxVersion()
|
||||
if (maxVersion && latestVersion && gt(latestVersion, maxVersion)) {
|
||||
logForDebugging(`AutoUpdater: maxVersion ${maxVersion} is set, capping update from ${latestVersion} to ${maxVersion}`);
|
||||
logForDebugging(
|
||||
`AutoUpdater: maxVersion ${maxVersion} is set, capping update from ${latestVersion} to ${maxVersion}`,
|
||||
)
|
||||
if (gte(currentVersion, maxVersion)) {
|
||||
logForDebugging(`AutoUpdater: current version ${currentVersion} is already at or above maxVersion ${maxVersion}, skipping update`);
|
||||
setVersions({
|
||||
global: currentVersion,
|
||||
latest: latestVersion
|
||||
});
|
||||
return;
|
||||
logForDebugging(
|
||||
`AutoUpdater: current version ${currentVersion} is already at or above maxVersion ${maxVersion}, skipping update`,
|
||||
)
|
||||
setVersions({ global: currentVersion, latest: latestVersion })
|
||||
return
|
||||
}
|
||||
latestVersion = maxVersion;
|
||||
latestVersion = maxVersion
|
||||
}
|
||||
setVersions({
|
||||
global: currentVersion,
|
||||
latest: latestVersion
|
||||
});
|
||||
|
||||
setVersions({ global: currentVersion, latest: latestVersion })
|
||||
|
||||
// Check if update needed and perform update
|
||||
if (!isDisabled && currentVersion && latestVersion && !gte(currentVersion, latestVersion) && !shouldSkipVersion(latestVersion)) {
|
||||
const startTime = Date.now();
|
||||
onChangeIsUpdating(true);
|
||||
if (
|
||||
!isDisabled &&
|
||||
currentVersion &&
|
||||
latestVersion &&
|
||||
!gte(currentVersion, latestVersion) &&
|
||||
!shouldSkipVersion(latestVersion)
|
||||
) {
|
||||
const startTime = Date.now()
|
||||
onChangeIsUpdating(true)
|
||||
|
||||
// Remove native installer symlink since we're using JS-based updates
|
||||
// But only if user hasn't migrated to native installation
|
||||
const config = getGlobalConfig();
|
||||
const config = getGlobalConfig()
|
||||
if (config.installMethod !== 'native') {
|
||||
await removeInstalledSymlink();
|
||||
await removeInstalledSymlink()
|
||||
}
|
||||
|
||||
// Detect actual running installation type
|
||||
const installationType = await getCurrentInstallationType();
|
||||
logForDebugging(`AutoUpdater: Detected installation type: ${installationType}`);
|
||||
const installationType = await getCurrentInstallationType()
|
||||
logForDebugging(
|
||||
`AutoUpdater: Detected installation type: ${installationType}`,
|
||||
)
|
||||
|
||||
// Skip update for development builds
|
||||
if (installationType === 'development') {
|
||||
logForDebugging('AutoUpdater: Cannot auto-update development build');
|
||||
onChangeIsUpdating(false);
|
||||
return;
|
||||
logForDebugging('AutoUpdater: Cannot auto-update development build')
|
||||
onChangeIsUpdating(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Choose the appropriate update method based on what's actually running
|
||||
let installStatus: InstallStatus;
|
||||
let updateMethod: 'local' | 'global';
|
||||
let installStatus: InstallStatus
|
||||
let updateMethod: 'local' | 'global'
|
||||
|
||||
if (installationType === 'npm-local') {
|
||||
// Use local update for local installations
|
||||
logForDebugging('AutoUpdater: Using local update method');
|
||||
updateMethod = 'local';
|
||||
installStatus = await installOrUpdateClaudePackage(channel);
|
||||
logForDebugging('AutoUpdater: Using local update method')
|
||||
updateMethod = 'local'
|
||||
installStatus = await installOrUpdateClaudePackage(channel)
|
||||
} else if (installationType === 'npm-global') {
|
||||
// Use global update for global installations
|
||||
logForDebugging('AutoUpdater: Using global update method');
|
||||
updateMethod = 'global';
|
||||
installStatus = await installGlobalPackage();
|
||||
logForDebugging('AutoUpdater: Using global update method')
|
||||
updateMethod = 'global'
|
||||
installStatus = await installGlobalPackage()
|
||||
} else if (installationType === 'native') {
|
||||
// This shouldn't happen - native should use NativeAutoUpdater
|
||||
logForDebugging('AutoUpdater: Unexpected native installation in non-native updater');
|
||||
onChangeIsUpdating(false);
|
||||
return;
|
||||
logForDebugging(
|
||||
'AutoUpdater: Unexpected native installation in non-native updater',
|
||||
)
|
||||
onChangeIsUpdating(false)
|
||||
return
|
||||
} else {
|
||||
// Fallback to config-based detection for unknown types
|
||||
logForDebugging(`AutoUpdater: Unknown installation type, falling back to config`);
|
||||
const isMigrated = config.installMethod === 'local';
|
||||
updateMethod = isMigrated ? 'local' : 'global';
|
||||
logForDebugging(
|
||||
`AutoUpdater: Unknown installation type, falling back to config`,
|
||||
)
|
||||
const isMigrated = config.installMethod === 'local'
|
||||
updateMethod = isMigrated ? 'local' : 'global'
|
||||
|
||||
if (isMigrated) {
|
||||
installStatus = await installOrUpdateClaudePackage(channel);
|
||||
installStatus = await installOrUpdateClaudePackage(channel)
|
||||
} else {
|
||||
installStatus = await installGlobalPackage();
|
||||
installStatus = await installGlobalPackage()
|
||||
}
|
||||
}
|
||||
onChangeIsUpdating(false);
|
||||
|
||||
onChangeIsUpdating(false)
|
||||
|
||||
if (installStatus === 'success') {
|
||||
logEvent('tengu_auto_updater_success', {
|
||||
fromVersion: currentVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
toVersion: latestVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
fromVersion:
|
||||
currentVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
toVersion:
|
||||
latestVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
durationMs: Date.now() - startTime,
|
||||
wasMigrated: updateMethod === 'local',
|
||||
installationType: installationType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
installationType:
|
||||
installationType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
} else {
|
||||
logEvent('tengu_auto_updater_fail', {
|
||||
fromVersion: currentVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
attemptedVersion: latestVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
status: installStatus as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
fromVersion:
|
||||
currentVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
attemptedVersion:
|
||||
latestVersion as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
status:
|
||||
installStatus as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
durationMs: Date.now() - startTime,
|
||||
wasMigrated: updateMethod === 'local',
|
||||
installationType: installationType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
installationType:
|
||||
installationType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
}
|
||||
|
||||
onAutoUpdaterResult({
|
||||
version: latestVersion,
|
||||
status: installStatus
|
||||
});
|
||||
status: installStatus,
|
||||
})
|
||||
}
|
||||
// isUpdating intentionally omitted from deps; we read isUpdatingRef
|
||||
// instead so the guard is always current without changing callback
|
||||
// identity (which would re-trigger the initial-check useEffect below).
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: isUpdating read via ref
|
||||
}, [onAutoUpdaterResult]);
|
||||
}, [onAutoUpdaterResult])
|
||||
|
||||
// Initial check
|
||||
useEffect(() => {
|
||||
void checkForUpdates();
|
||||
}, [checkForUpdates]);
|
||||
void checkForUpdates()
|
||||
}, [checkForUpdates])
|
||||
|
||||
// Check every 30 minutes
|
||||
useInterval(checkForUpdates, 30 * 60 * 1000);
|
||||
useInterval(checkForUpdates, 30 * 60 * 1000)
|
||||
|
||||
if (!autoUpdaterResult?.version && (!versions.global || !versions.latest)) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
if (!autoUpdaterResult?.version && !isUpdating) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
return <Box flexDirection="row" gap={1}>
|
||||
{verbose && <Text dimColor wrap="truncate">
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" gap={1}>
|
||||
{verbose && (
|
||||
<Text dimColor wrap="truncate">
|
||||
globalVersion: {versions.global} · latestVersion:{' '}
|
||||
{versions.latest}
|
||||
</Text>}
|
||||
{isUpdating ? <>
|
||||
</Text>
|
||||
)}
|
||||
{isUpdating ? (
|
||||
<>
|
||||
<Box>
|
||||
<Text color="text" dimColor wrap="truncate">
|
||||
Auto-updating…
|
||||
</Text>
|
||||
</Box>
|
||||
</> : autoUpdaterResult?.status === 'success' && showSuccessMessage && updateSemver && <Text color="success" wrap="truncate">
|
||||
</>
|
||||
) : (
|
||||
autoUpdaterResult?.status === 'success' &&
|
||||
showSuccessMessage &&
|
||||
updateSemver && (
|
||||
<Text color="success" wrap="truncate">
|
||||
✓ Update installed · Restart to apply
|
||||
</Text>}
|
||||
{(autoUpdaterResult?.status === 'install_failed' || autoUpdaterResult?.status === 'no_permissions') && <Text color="error" wrap="truncate">
|
||||
</Text>
|
||||
)
|
||||
)}
|
||||
{(autoUpdaterResult?.status === 'install_failed' ||
|
||||
autoUpdaterResult?.status === 'no_permissions') && (
|
||||
<Text color="error" wrap="truncate">
|
||||
✗ Auto-update failed · Try <Text bold>claude doctor</Text> or{' '}
|
||||
<Text bold>
|
||||
{hasLocalInstall ? `cd ~/.claude/local && npm update ${MACRO.PACKAGE_URL}` : `npm i -g ${MACRO.PACKAGE_URL}`}
|
||||
{hasLocalInstall
|
||||
? `cd ~/.claude/local && npm update ${MACRO.PACKAGE_URL}`
|
||||
: `npm i -g ${MACRO.PACKAGE_URL}`}
|
||||
</Text>
|
||||
</Text>}
|
||||
</Box>;
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,90 +1,90 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import * as React from 'react';
|
||||
import type { AutoUpdaterResult } from '../utils/autoUpdater.js';
|
||||
import { isAutoUpdaterDisabled } from '../utils/config.js';
|
||||
import { logForDebugging } from '../utils/debug.js';
|
||||
import { getCurrentInstallationType } from '../utils/doctorDiagnostic.js';
|
||||
import { AutoUpdater } from './AutoUpdater.js';
|
||||
import { NativeAutoUpdater } from './NativeAutoUpdater.js';
|
||||
import { PackageManagerAutoUpdater } from './PackageManagerAutoUpdater.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import * as React from 'react'
|
||||
import type { AutoUpdaterResult } from '../utils/autoUpdater.js'
|
||||
import { isAutoUpdaterDisabled } from '../utils/config.js'
|
||||
import { logForDebugging } from '../utils/debug.js'
|
||||
import { getCurrentInstallationType } from '../utils/doctorDiagnostic.js'
|
||||
import { AutoUpdater } from './AutoUpdater.js'
|
||||
import { NativeAutoUpdater } from './NativeAutoUpdater.js'
|
||||
import { PackageManagerAutoUpdater } from './PackageManagerAutoUpdater.js'
|
||||
|
||||
type Props = {
|
||||
isUpdating: boolean;
|
||||
onChangeIsUpdating: (isUpdating: boolean) => void;
|
||||
onAutoUpdaterResult: (autoUpdaterResult: AutoUpdaterResult) => void;
|
||||
autoUpdaterResult: AutoUpdaterResult | null;
|
||||
showSuccessMessage: boolean;
|
||||
verbose: boolean;
|
||||
};
|
||||
export function AutoUpdaterWrapper(t0) {
|
||||
const $ = _c(17);
|
||||
const {
|
||||
isUpdating,
|
||||
onChangeIsUpdating,
|
||||
onAutoUpdaterResult,
|
||||
autoUpdaterResult,
|
||||
showSuccessMessage,
|
||||
verbose
|
||||
} = t0;
|
||||
const [useNativeInstaller, setUseNativeInstaller] = React.useState(null);
|
||||
const [isPackageManager, setIsPackageManager] = React.useState(null);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = () => {
|
||||
const checkInstallation = async function checkInstallation() {
|
||||
if (feature("SKIP_DETECTION_WHEN_AUTOUPDATES_DISABLED") && isAutoUpdaterDisabled()) {
|
||||
logForDebugging("AutoUpdaterWrapper: Skipping detection, auto-updates disabled");
|
||||
return;
|
||||
}
|
||||
const installationType = await getCurrentInstallationType();
|
||||
logForDebugging(`AutoUpdaterWrapper: Installation type: ${installationType}`);
|
||||
setUseNativeInstaller(installationType === "native");
|
||||
setIsPackageManager(installationType === "package-manager");
|
||||
};
|
||||
checkInstallation();
|
||||
};
|
||||
t2 = [];
|
||||
$[0] = t1;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
t2 = $[1];
|
||||
}
|
||||
React.useEffect(t1, t2);
|
||||
if (useNativeInstaller === null || isPackageManager === null) {
|
||||
return null;
|
||||
}
|
||||
if (isPackageManager) {
|
||||
let t3;
|
||||
if ($[2] !== autoUpdaterResult || $[3] !== isUpdating || $[4] !== onAutoUpdaterResult || $[5] !== onChangeIsUpdating || $[6] !== showSuccessMessage || $[7] !== verbose) {
|
||||
t3 = <PackageManagerAutoUpdater verbose={verbose} onAutoUpdaterResult={onAutoUpdaterResult} autoUpdaterResult={autoUpdaterResult} isUpdating={isUpdating} onChangeIsUpdating={onChangeIsUpdating} showSuccessMessage={showSuccessMessage} />;
|
||||
$[2] = autoUpdaterResult;
|
||||
$[3] = isUpdating;
|
||||
$[4] = onAutoUpdaterResult;
|
||||
$[5] = onChangeIsUpdating;
|
||||
$[6] = showSuccessMessage;
|
||||
$[7] = verbose;
|
||||
$[8] = t3;
|
||||
} else {
|
||||
t3 = $[8];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
const Updater = useNativeInstaller ? NativeAutoUpdater : AutoUpdater;
|
||||
let t3;
|
||||
if ($[9] !== Updater || $[10] !== autoUpdaterResult || $[11] !== isUpdating || $[12] !== onAutoUpdaterResult || $[13] !== onChangeIsUpdating || $[14] !== showSuccessMessage || $[15] !== verbose) {
|
||||
t3 = <Updater verbose={verbose} onAutoUpdaterResult={onAutoUpdaterResult} autoUpdaterResult={autoUpdaterResult} isUpdating={isUpdating} onChangeIsUpdating={onChangeIsUpdating} showSuccessMessage={showSuccessMessage} />;
|
||||
$[9] = Updater;
|
||||
$[10] = autoUpdaterResult;
|
||||
$[11] = isUpdating;
|
||||
$[12] = onAutoUpdaterResult;
|
||||
$[13] = onChangeIsUpdating;
|
||||
$[14] = showSuccessMessage;
|
||||
$[15] = verbose;
|
||||
$[16] = t3;
|
||||
} else {
|
||||
t3 = $[16];
|
||||
}
|
||||
return t3;
|
||||
isUpdating: boolean
|
||||
onChangeIsUpdating: (isUpdating: boolean) => void
|
||||
onAutoUpdaterResult: (autoUpdaterResult: AutoUpdaterResult) => void
|
||||
autoUpdaterResult: AutoUpdaterResult | null
|
||||
showSuccessMessage: boolean
|
||||
verbose: boolean
|
||||
}
|
||||
|
||||
export function AutoUpdaterWrapper({
|
||||
isUpdating,
|
||||
onChangeIsUpdating,
|
||||
onAutoUpdaterResult,
|
||||
autoUpdaterResult,
|
||||
showSuccessMessage,
|
||||
verbose,
|
||||
}: Props): React.ReactNode {
|
||||
const [useNativeInstaller, setUseNativeInstaller] = React.useState<
|
||||
boolean | null
|
||||
>(null)
|
||||
const [isPackageManager, setIsPackageManager] = React.useState<
|
||||
boolean | null
|
||||
>(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
async function checkInstallation() {
|
||||
// Skip installation type detection if auto-updates are disabled (ant-only)
|
||||
// This avoids potentially slow package manager detection (spawnSync calls)
|
||||
if (
|
||||
feature('SKIP_DETECTION_WHEN_AUTOUPDATES_DISABLED') &&
|
||||
isAutoUpdaterDisabled()
|
||||
) {
|
||||
logForDebugging(
|
||||
'AutoUpdaterWrapper: Skipping detection, auto-updates disabled',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const installationType = await getCurrentInstallationType()
|
||||
logForDebugging(
|
||||
`AutoUpdaterWrapper: Installation type: ${installationType}`,
|
||||
)
|
||||
setUseNativeInstaller(installationType === 'native')
|
||||
setIsPackageManager(installationType === 'package-manager')
|
||||
}
|
||||
|
||||
void checkInstallation()
|
||||
}, [])
|
||||
|
||||
// Don't render until we know the installation type
|
||||
if (useNativeInstaller === null || isPackageManager === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (isPackageManager) {
|
||||
return (
|
||||
<PackageManagerAutoUpdater
|
||||
verbose={verbose}
|
||||
onAutoUpdaterResult={onAutoUpdaterResult}
|
||||
autoUpdaterResult={autoUpdaterResult}
|
||||
isUpdating={isUpdating}
|
||||
onChangeIsUpdating={onChangeIsUpdating}
|
||||
showSuccessMessage={showSuccessMessage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Updater = useNativeInstaller ? NativeAutoUpdater : AutoUpdater
|
||||
|
||||
return (
|
||||
<Updater
|
||||
verbose={verbose}
|
||||
onAutoUpdaterResult={onAutoUpdaterResult}
|
||||
autoUpdaterResult={autoUpdaterResult}
|
||||
isUpdating={isUpdating}
|
||||
onChangeIsUpdating={onChangeIsUpdating}
|
||||
showSuccessMessage={showSuccessMessage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,81 +1,76 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Box, Link, Text } from '../ink.js';
|
||||
import { type AwsAuthStatus, AwsAuthStatusManager } from '../utils/awsAuthStatusManager.js';
|
||||
const URL_RE = /https?:\/\/\S+/;
|
||||
export function AwsAuthStatusBox() {
|
||||
const $ = _c(11);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = AwsAuthStatusManager.getInstance().getStatus();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const [status, setStatus] = useState(t0);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = () => {
|
||||
const unsubscribe = AwsAuthStatusManager.getInstance().subscribe(setStatus);
|
||||
return unsubscribe;
|
||||
};
|
||||
t2 = [];
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Box, Link, Text } from '../ink.js'
|
||||
import {
|
||||
type AwsAuthStatus,
|
||||
AwsAuthStatusManager,
|
||||
} from '../utils/awsAuthStatusManager.js'
|
||||
|
||||
const URL_RE = /https?:\/\/\S+/
|
||||
|
||||
export function AwsAuthStatusBox(): React.ReactNode {
|
||||
const [status, setStatus] = useState<AwsAuthStatus>(
|
||||
AwsAuthStatusManager.getInstance().getStatus(),
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// Subscribe to status updates
|
||||
const unsubscribe = AwsAuthStatusManager.getInstance().subscribe(setStatus)
|
||||
return unsubscribe
|
||||
}, [])
|
||||
|
||||
// Don't show anything if not authenticating and no error
|
||||
if (!status.isAuthenticating && !status.error && status.output.length === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Don't show if authentication succeeded (no error and not authenticating)
|
||||
if (!status.isAuthenticating && !status.error) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
let t3;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Text bold={true} color="permission">Cloud Authentication</Text>;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
let t4;
|
||||
if ($[4] !== status.output) {
|
||||
t4 = status.output.length > 0 && <Box flexDirection="column" marginTop={1}>{status.output.slice(-5).map(_temp)}</Box>;
|
||||
$[4] = status.output;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
let t5;
|
||||
if ($[6] !== status.error) {
|
||||
t5 = status.error && <Box marginTop={1}><Text color="error">{status.error}</Text></Box>;
|
||||
$[6] = status.error;
|
||||
$[7] = t5;
|
||||
} else {
|
||||
t5 = $[7];
|
||||
}
|
||||
let t6;
|
||||
if ($[8] !== t4 || $[9] !== t5) {
|
||||
t6 = <Box flexDirection="column" borderStyle="round" borderColor="permission" paddingX={1} marginY={1}>{t3}{t4}{t5}</Box>;
|
||||
$[8] = t4;
|
||||
$[9] = t5;
|
||||
$[10] = t6;
|
||||
} else {
|
||||
t6 = $[10];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
function _temp(line, index) {
|
||||
const m = line.match(URL_RE);
|
||||
if (!m) {
|
||||
return <Text key={index} dimColor={true}>{line}</Text>;
|
||||
}
|
||||
const url = m[0];
|
||||
const start = m.index ?? 0;
|
||||
const before = line.slice(0, start);
|
||||
const after = line.slice(start + url.length);
|
||||
return <Text key={index} dimColor={true}>{before}<Link url={url}>{url}</Link>{after}</Text>;
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor="permission"
|
||||
paddingX={1}
|
||||
marginY={1}
|
||||
>
|
||||
<Text bold color="permission">
|
||||
Cloud Authentication
|
||||
</Text>
|
||||
|
||||
{status.output.length > 0 && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
{status.output.slice(-5).map((line, index) => {
|
||||
const m = line.match(URL_RE)
|
||||
if (!m) {
|
||||
return (
|
||||
<Text key={index} dimColor>
|
||||
{line}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
const url = m[0]
|
||||
const start = m.index ?? 0
|
||||
const before = line.slice(0, start)
|
||||
const after = line.slice(start + url.length)
|
||||
return (
|
||||
<Text key={index} dimColor>
|
||||
{before}
|
||||
<Link url={url}>{url}</Link>
|
||||
{after}
|
||||
</Text>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{status.error && (
|
||||
<Box marginTop={1}>
|
||||
<Text color="error">{status.error}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,135 +1,162 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { renderPlaceholder } from '../hooks/renderPlaceholder.js';
|
||||
import { usePasteHandler } from '../hooks/usePasteHandler.js';
|
||||
import { useDeclaredCursor } from '../ink/hooks/use-declared-cursor.js';
|
||||
import { Ansi, Box, Text, useInput } from '../ink.js';
|
||||
import type { BaseInputState, BaseTextInputProps } from '../types/textInputTypes.js';
|
||||
import type { TextHighlight } from '../utils/textHighlighting.js';
|
||||
import { HighlightedInput } from './PromptInput/ShimmeredInput.js';
|
||||
import React from 'react'
|
||||
import { renderPlaceholder } from '../hooks/renderPlaceholder.js'
|
||||
import { usePasteHandler } from '../hooks/usePasteHandler.js'
|
||||
import { useDeclaredCursor } from '../ink/hooks/use-declared-cursor.js'
|
||||
import { Ansi, Box, Text, useInput } from '../ink.js'
|
||||
import type {
|
||||
BaseInputState,
|
||||
BaseTextInputProps,
|
||||
} from '../types/textInputTypes.js'
|
||||
import type { TextHighlight } from '../utils/textHighlighting.js'
|
||||
import { HighlightedInput } from './PromptInput/ShimmeredInput.js'
|
||||
|
||||
type BaseTextInputComponentProps = BaseTextInputProps & {
|
||||
inputState: BaseInputState;
|
||||
children?: React.ReactNode;
|
||||
terminalFocus: boolean;
|
||||
highlights?: TextHighlight[];
|
||||
invert?: (text: string) => string;
|
||||
hidePlaceholderText?: boolean;
|
||||
};
|
||||
inputState: BaseInputState
|
||||
children?: React.ReactNode
|
||||
terminalFocus: boolean
|
||||
highlights?: TextHighlight[]
|
||||
invert?: (text: string) => string
|
||||
hidePlaceholderText?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A base component for text inputs that handles rendering and basic input
|
||||
*/
|
||||
export function BaseTextInput(t0) {
|
||||
const $ = _c(14);
|
||||
const {
|
||||
inputState,
|
||||
children,
|
||||
terminalFocus,
|
||||
invert,
|
||||
hidePlaceholderText,
|
||||
...props
|
||||
} = t0;
|
||||
const {
|
||||
onInput,
|
||||
renderedValue,
|
||||
cursorLine,
|
||||
cursorColumn
|
||||
} = inputState;
|
||||
const t1 = Boolean(props.focus && props.showCursor && terminalFocus);
|
||||
let t2;
|
||||
if ($[0] !== cursorColumn || $[1] !== cursorLine || $[2] !== t1) {
|
||||
t2 = {
|
||||
line: cursorLine,
|
||||
column: cursorColumn,
|
||||
active: t1
|
||||
};
|
||||
$[0] = cursorColumn;
|
||||
$[1] = cursorLine;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const cursorRef = useDeclaredCursor(t2);
|
||||
const {
|
||||
wrappedOnInput,
|
||||
isPasting: t3
|
||||
} = usePasteHandler({
|
||||
export function BaseTextInput({
|
||||
inputState,
|
||||
children,
|
||||
terminalFocus,
|
||||
invert,
|
||||
hidePlaceholderText,
|
||||
...props
|
||||
}: BaseTextInputComponentProps): React.ReactNode {
|
||||
const { onInput, renderedValue, cursorLine, cursorColumn } = inputState
|
||||
|
||||
// Park the native terminal cursor at the input caret. Terminal emulators
|
||||
// position IME preedit text at the physical cursor, and screen readers /
|
||||
// screen magnifiers track it — so parking here makes CJK input appear
|
||||
// inline and lets accessibility tools follow the input. The Box ref below
|
||||
// is the yoga layout origin; (cursorLine, cursorColumn) is relative to it.
|
||||
// Only active when the input is focused, showing its cursor, and the
|
||||
// terminal itself has focus.
|
||||
const cursorRef = useDeclaredCursor({
|
||||
line: cursorLine,
|
||||
column: cursorColumn,
|
||||
active: Boolean(props.focus && props.showCursor && terminalFocus),
|
||||
})
|
||||
|
||||
const { wrappedOnInput, isPasting } = usePasteHandler({
|
||||
onPaste: props.onPaste,
|
||||
onInput: (input, key) => {
|
||||
// Prevent Enter key from triggering submission during paste
|
||||
if (isPasting && key.return) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
onInput(input, key);
|
||||
onInput(input, key)
|
||||
},
|
||||
onImagePaste: props.onImagePaste
|
||||
});
|
||||
const isPasting = t3;
|
||||
const {
|
||||
onIsPastingChange
|
||||
} = props;
|
||||
onImagePaste: props.onImagePaste,
|
||||
})
|
||||
|
||||
// Notify parent when paste state changes
|
||||
const { onIsPastingChange } = props
|
||||
React.useEffect(() => {
|
||||
if (onIsPastingChange) {
|
||||
onIsPastingChange(isPasting);
|
||||
onIsPastingChange(isPasting)
|
||||
}
|
||||
}, [isPasting, onIsPastingChange]);
|
||||
const {
|
||||
showPlaceholder,
|
||||
renderedPlaceholder
|
||||
} = renderPlaceholder({
|
||||
}, [isPasting, onIsPastingChange])
|
||||
|
||||
const { showPlaceholder, renderedPlaceholder } = renderPlaceholder({
|
||||
placeholder: props.placeholder,
|
||||
value: props.value,
|
||||
showCursor: props.showCursor,
|
||||
focus: props.focus,
|
||||
terminalFocus,
|
||||
invert,
|
||||
hidePlaceholderText
|
||||
});
|
||||
useInput(wrappedOnInput, {
|
||||
isActive: props.focus
|
||||
});
|
||||
const commandWithoutArgs = props.value && props.value.trim().indexOf(" ") === -1 || props.value && props.value.endsWith(" ");
|
||||
const showArgumentHint = Boolean(props.argumentHint && props.value && commandWithoutArgs && props.value.startsWith("/"));
|
||||
const cursorFiltered = props.showCursor && props.highlights ? props.highlights.filter(h => h.dimColor || props.cursorOffset < h.start || props.cursorOffset >= h.end) : props.highlights;
|
||||
const {
|
||||
viewportCharOffset,
|
||||
viewportCharEnd
|
||||
} = inputState;
|
||||
const filteredHighlights = cursorFiltered && viewportCharOffset > 0 ? cursorFiltered.filter(h_0 => h_0.end > viewportCharOffset && h_0.start < viewportCharEnd).map(h_1 => ({
|
||||
...h_1,
|
||||
start: Math.max(0, h_1.start - viewportCharOffset),
|
||||
end: h_1.end - viewportCharOffset
|
||||
})) : cursorFiltered;
|
||||
const hasHighlights = filteredHighlights && filteredHighlights.length > 0;
|
||||
hidePlaceholderText,
|
||||
})
|
||||
|
||||
useInput(wrappedOnInput, { isActive: props.focus })
|
||||
|
||||
// Show argument hint only when we have a value and the hint is provided
|
||||
// Only show the argument hint when:
|
||||
// 1. We have a hint to show
|
||||
// 2. We have a command typed (value is not empty)
|
||||
// 3. The command doesn't have arguments yet (no text after the space)
|
||||
// 4. We're actually typing a command (the value starts with /)
|
||||
const commandWithoutArgs =
|
||||
(props.value && props.value.trim().indexOf(' ') === -1) ||
|
||||
(props.value && props.value.endsWith(' '))
|
||||
|
||||
const showArgumentHint = Boolean(
|
||||
props.argumentHint &&
|
||||
props.value &&
|
||||
commandWithoutArgs &&
|
||||
props.value.startsWith('/'),
|
||||
)
|
||||
|
||||
// Filter out highlights that contain the cursor position
|
||||
const cursorFiltered =
|
||||
props.showCursor && props.highlights
|
||||
? props.highlights.filter(
|
||||
h =>
|
||||
h.dimColor ||
|
||||
props.cursorOffset < h.start ||
|
||||
props.cursorOffset >= h.end,
|
||||
)
|
||||
: props.highlights
|
||||
|
||||
// Adjust highlights for viewport windowing: highlight positions reference the
|
||||
// full input text, but renderedValue only contains the windowed subset.
|
||||
const { viewportCharOffset, viewportCharEnd } = inputState
|
||||
const filteredHighlights =
|
||||
cursorFiltered && viewportCharOffset > 0
|
||||
? cursorFiltered
|
||||
.filter(h => h.end > viewportCharOffset && h.start < viewportCharEnd)
|
||||
.map(h => ({
|
||||
...h,
|
||||
start: Math.max(0, h.start - viewportCharOffset),
|
||||
end: h.end - viewportCharOffset,
|
||||
}))
|
||||
: cursorFiltered
|
||||
|
||||
const hasHighlights = filteredHighlights && filteredHighlights.length > 0
|
||||
|
||||
if (hasHighlights) {
|
||||
return <Box ref={cursorRef}><HighlightedInput text={renderedValue} highlights={filteredHighlights} />{showArgumentHint && <Text dimColor={true}>{props.value?.endsWith(" ") ? "" : " "}{props.argumentHint}</Text>}{children}</Box>;
|
||||
return (
|
||||
<Box ref={cursorRef}>
|
||||
<HighlightedInput
|
||||
text={renderedValue}
|
||||
highlights={filteredHighlights}
|
||||
/>
|
||||
{showArgumentHint && (
|
||||
<Text dimColor>
|
||||
{props.value?.endsWith(' ') ? '' : ' '}
|
||||
{props.argumentHint}
|
||||
</Text>
|
||||
)}
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
const T0 = Box;
|
||||
const T1 = Text;
|
||||
const t4 = "truncate-end";
|
||||
const t5 = showPlaceholder && props.placeholderElement ? props.placeholderElement : showPlaceholder && renderedPlaceholder ? <Ansi>{renderedPlaceholder}</Ansi> : <Ansi>{renderedValue}</Ansi>;
|
||||
const t6 = showArgumentHint && <Text dimColor={true}>{props.value?.endsWith(" ") ? "" : " "}{props.argumentHint}</Text>;
|
||||
let t7;
|
||||
if ($[4] !== T1 || $[5] !== children || $[6] !== props || $[7] !== t5 || $[8] !== t6) {
|
||||
t7 = <T1 wrap={t4} dimColor={props.dimColor}>{t5}{t6}{children}</T1>;
|
||||
$[4] = T1;
|
||||
$[5] = children;
|
||||
$[6] = props;
|
||||
$[7] = t5;
|
||||
$[8] = t6;
|
||||
$[9] = t7;
|
||||
} else {
|
||||
t7 = $[9];
|
||||
}
|
||||
let t8;
|
||||
if ($[10] !== T0 || $[11] !== cursorRef || $[12] !== t7) {
|
||||
t8 = <T0 ref={cursorRef}>{t7}</T0>;
|
||||
$[10] = T0;
|
||||
$[11] = cursorRef;
|
||||
$[12] = t7;
|
||||
$[13] = t8;
|
||||
} else {
|
||||
t8 = $[13];
|
||||
}
|
||||
return t8;
|
||||
|
||||
return (
|
||||
<Box ref={cursorRef}>
|
||||
<Text wrap="truncate-end" dimColor={props.dimColor}>
|
||||
{showPlaceholder && props.placeholderElement ? (
|
||||
props.placeholderElement
|
||||
) : showPlaceholder && renderedPlaceholder ? (
|
||||
<Ansi>{renderedPlaceholder}</Ansi>
|
||||
) : (
|
||||
<Ansi>{renderedValue}</Ansi>
|
||||
)}
|
||||
{showArgumentHint && (
|
||||
<Text dimColor>
|
||||
{props.value?.endsWith(' ') ? '' : ' '}
|
||||
{props.argumentHint}
|
||||
</Text>
|
||||
)}
|
||||
{children}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,55 +1,42 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Box } from '../ink.js';
|
||||
import { BashTool } from '../tools/BashTool/BashTool.js';
|
||||
import type { ShellProgress } from '../types/tools.js';
|
||||
import { UserBashInputMessage } from './messages/UserBashInputMessage.js';
|
||||
import { ShellProgressMessage } from './shell/ShellProgressMessage.js';
|
||||
import React from 'react'
|
||||
import { Box } from '../ink.js'
|
||||
import { BashTool } from '../tools/BashTool/BashTool.js'
|
||||
import type { ShellProgress } from '../types/tools.js'
|
||||
import { UserBashInputMessage } from './messages/UserBashInputMessage.js'
|
||||
import { ShellProgressMessage } from './shell/ShellProgressMessage.js'
|
||||
|
||||
type Props = {
|
||||
input: string;
|
||||
progress: ShellProgress | null;
|
||||
verbose: boolean;
|
||||
};
|
||||
export function BashModeProgress(t0) {
|
||||
const $ = _c(8);
|
||||
const {
|
||||
input,
|
||||
progress,
|
||||
verbose
|
||||
} = t0;
|
||||
const t1 = `<bash-input>${input}</bash-input>`;
|
||||
let t2;
|
||||
if ($[0] !== t1) {
|
||||
t2 = <UserBashInputMessage addMargin={false} param={{
|
||||
text: t1,
|
||||
type: "text"
|
||||
}} />;
|
||||
$[0] = t1;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
let t3;
|
||||
if ($[2] !== progress || $[3] !== verbose) {
|
||||
t3 = progress ? <ShellProgressMessage fullOutput={progress.fullOutput} output={progress.output} elapsedTimeSeconds={progress.elapsedTimeSeconds} totalLines={progress.totalLines} verbose={verbose} /> : BashTool.renderToolUseProgressMessage?.([], {
|
||||
verbose,
|
||||
tools: [],
|
||||
terminalSize: undefined
|
||||
});
|
||||
$[2] = progress;
|
||||
$[3] = verbose;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== t2 || $[6] !== t3) {
|
||||
t4 = <Box flexDirection="column" marginTop={1}>{t2}{t3}</Box>;
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
return t4;
|
||||
input: string
|
||||
progress: ShellProgress | null
|
||||
verbose: boolean
|
||||
}
|
||||
|
||||
export function BashModeProgress({
|
||||
input,
|
||||
progress,
|
||||
verbose,
|
||||
}: Props): React.ReactNode {
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<UserBashInputMessage
|
||||
addMargin={false}
|
||||
param={{ text: `<bash-input>${input}</bash-input>`, type: 'text' }}
|
||||
/>
|
||||
{progress ? (
|
||||
<ShellProgressMessage
|
||||
fullOutput={progress.fullOutput}
|
||||
output={progress.output}
|
||||
elapsedTimeSeconds={progress.elapsedTimeSeconds}
|
||||
totalLines={progress.totalLines}
|
||||
verbose={verbose}
|
||||
/>
|
||||
) : (
|
||||
BashTool.renderToolUseProgressMessage?.([], {
|
||||
verbose,
|
||||
tools: [],
|
||||
terminalSize: undefined,
|
||||
})
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,400 +1,160 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { basename } from 'path';
|
||||
import { toString as qrToString } from 'qrcode';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getOriginalCwd } from '../bootstrap/state.js';
|
||||
import { buildActiveFooterText, buildIdleFooterText, FAILED_FOOTER_TEXT, getBridgeStatus } from '../bridge/bridgeStatusUtil.js';
|
||||
import { BRIDGE_FAILED_INDICATOR, BRIDGE_READY_INDICATOR } from '../constants/figures.js';
|
||||
import { useRegisterOverlay } from '../context/overlayContext.js';
|
||||
import { basename } from 'path'
|
||||
import { toString as qrToString } from 'qrcode'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { getOriginalCwd } from '../bootstrap/state.js'
|
||||
import {
|
||||
buildActiveFooterText,
|
||||
buildIdleFooterText,
|
||||
FAILED_FOOTER_TEXT,
|
||||
getBridgeStatus,
|
||||
} from '../bridge/bridgeStatusUtil.js'
|
||||
import {
|
||||
BRIDGE_FAILED_INDICATOR,
|
||||
BRIDGE_READY_INDICATOR,
|
||||
} from '../constants/figures.js'
|
||||
import { useRegisterOverlay } from '../context/overlayContext.js'
|
||||
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw 'd' key for disconnect, not a configurable keybinding action
|
||||
import { Box, Text, useInput } from '../ink.js';
|
||||
import { useKeybindings } from '../keybindings/useKeybinding.js';
|
||||
import { useAppState, useSetAppState } from '../state/AppState.js';
|
||||
import { saveGlobalConfig } from '../utils/config.js';
|
||||
import { getBranch } from '../utils/git.js';
|
||||
import { Dialog } from './design-system/Dialog.js';
|
||||
import { Box, Text, useInput } from '../ink.js'
|
||||
import { useKeybindings } from '../keybindings/useKeybinding.js'
|
||||
import { useAppState, useSetAppState } from '../state/AppState.js'
|
||||
import { saveGlobalConfig } from '../utils/config.js'
|
||||
import { getBranch } from '../utils/git.js'
|
||||
import { Dialog } from './design-system/Dialog.js'
|
||||
|
||||
type Props = {
|
||||
onDone: () => void;
|
||||
};
|
||||
export function BridgeDialog(t0) {
|
||||
const $ = _c(87);
|
||||
const {
|
||||
onDone
|
||||
} = t0;
|
||||
useRegisterOverlay("bridge-dialog", undefined);
|
||||
const connected = useAppState(_temp);
|
||||
const sessionActive = useAppState(_temp2);
|
||||
const reconnecting = useAppState(_temp3);
|
||||
const connectUrl = useAppState(_temp4);
|
||||
const sessionUrl = useAppState(_temp5);
|
||||
const error = useAppState(_temp6);
|
||||
const explicit = useAppState(_temp7);
|
||||
const environmentId = useAppState(_temp8);
|
||||
const sessionId = useAppState(_temp9);
|
||||
const verbose = useAppState(_temp0);
|
||||
const setAppState = useSetAppState();
|
||||
const [showQR, setShowQR] = useState(false);
|
||||
const [qrText, setQrText] = useState("");
|
||||
const [branchName, setBranchName] = useState("");
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = basename(getOriginalCwd());
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const repoName = t1;
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = () => {
|
||||
getBranch().then(setBranchName).catch(_temp1);
|
||||
};
|
||||
t3 = [];
|
||||
$[1] = t2;
|
||||
$[2] = t3;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
t3 = $[2];
|
||||
}
|
||||
useEffect(t2, t3);
|
||||
const displayUrl = sessionActive ? sessionUrl : connectUrl;
|
||||
let t4;
|
||||
let t5;
|
||||
if ($[3] !== displayUrl || $[4] !== showQR) {
|
||||
t4 = () => {
|
||||
if (!showQR || !displayUrl) {
|
||||
setQrText("");
|
||||
return;
|
||||
onDone: () => void
|
||||
}
|
||||
|
||||
export function BridgeDialog({ onDone }: Props): React.ReactNode {
|
||||
useRegisterOverlay('bridge-dialog')
|
||||
|
||||
const connected = useAppState(s => s.replBridgeConnected)
|
||||
const sessionActive = useAppState(s => s.replBridgeSessionActive)
|
||||
const reconnecting = useAppState(s => s.replBridgeReconnecting)
|
||||
const connectUrl = useAppState(s => s.replBridgeConnectUrl)
|
||||
const sessionUrl = useAppState(s => s.replBridgeSessionUrl)
|
||||
const error = useAppState(s => s.replBridgeError)
|
||||
const explicit = useAppState(s => s.replBridgeExplicit)
|
||||
const environmentId = useAppState(s => s.replBridgeEnvironmentId)
|
||||
const sessionId = useAppState(s => s.replBridgeSessionId)
|
||||
const verbose = useAppState(s => s.verbose)
|
||||
const setAppState = useSetAppState()
|
||||
|
||||
const [showQR, setShowQR] = useState(false)
|
||||
const [qrText, setQrText] = useState('')
|
||||
const [branchName, setBranchName] = useState('')
|
||||
|
||||
const repoName = basename(getOriginalCwd())
|
||||
|
||||
// Fetch branch name on mount
|
||||
useEffect(() => {
|
||||
getBranch()
|
||||
.then(setBranchName)
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
// The URL to display/QR: session URL when connected, connect URL when ready
|
||||
const displayUrl = sessionActive ? sessionUrl : connectUrl
|
||||
|
||||
// Generate QR code when URL changes or QR is toggled on
|
||||
useEffect(() => {
|
||||
if (!showQR || !displayUrl) {
|
||||
setQrText('')
|
||||
return
|
||||
}
|
||||
qrToString(displayUrl, {
|
||||
type: 'utf8',
|
||||
errorCorrectionLevel: 'L',
|
||||
small: true,
|
||||
})
|
||||
.then(setQrText)
|
||||
.catch(() => setQrText(''))
|
||||
}, [showQR, displayUrl])
|
||||
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:yes': onDone,
|
||||
'confirm:toggle': () => {
|
||||
setShowQR(prev => !prev)
|
||||
},
|
||||
},
|
||||
{ context: 'Confirmation' },
|
||||
)
|
||||
|
||||
useInput(input => {
|
||||
if (input === 'd') {
|
||||
// Persist opt-out only for CLI-flag/command-activated bridge.
|
||||
// Config-driven and GB-auto-connect users get session-only disconnect
|
||||
// — writing false would silently undo a Settings choice or opt a
|
||||
// GB-rollout user out permanently.
|
||||
if (explicit) {
|
||||
saveGlobalConfig(current => {
|
||||
if (current.remoteControlAtStartup === false) return current
|
||||
return { ...current, remoteControlAtStartup: false }
|
||||
})
|
||||
}
|
||||
qrToString(displayUrl, {
|
||||
type: "utf8",
|
||||
errorCorrectionLevel: "L",
|
||||
small: true
|
||||
}).then(setQrText).catch(() => setQrText(""));
|
||||
};
|
||||
t5 = [showQR, displayUrl];
|
||||
$[3] = displayUrl;
|
||||
$[4] = showQR;
|
||||
$[5] = t4;
|
||||
$[6] = t5;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
t5 = $[6];
|
||||
}
|
||||
useEffect(t4, t5);
|
||||
let t6;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = () => {
|
||||
setShowQR(_temp10);
|
||||
};
|
||||
$[7] = t6;
|
||||
} else {
|
||||
t6 = $[7];
|
||||
}
|
||||
let t7;
|
||||
if ($[8] !== onDone) {
|
||||
t7 = {
|
||||
"confirm:yes": onDone,
|
||||
"confirm:toggle": t6
|
||||
};
|
||||
$[8] = onDone;
|
||||
$[9] = t7;
|
||||
} else {
|
||||
t7 = $[9];
|
||||
}
|
||||
let t8;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[10] = t8;
|
||||
} else {
|
||||
t8 = $[10];
|
||||
}
|
||||
useKeybindings(t7, t8);
|
||||
let t9;
|
||||
if ($[11] !== explicit || $[12] !== onDone || $[13] !== setAppState) {
|
||||
t9 = input => {
|
||||
if (input === "d") {
|
||||
if (explicit) {
|
||||
saveGlobalConfig(_temp11);
|
||||
}
|
||||
setAppState(_temp12);
|
||||
onDone();
|
||||
}
|
||||
};
|
||||
$[11] = explicit;
|
||||
$[12] = onDone;
|
||||
$[13] = setAppState;
|
||||
$[14] = t9;
|
||||
} else {
|
||||
t9 = $[14];
|
||||
}
|
||||
useInput(t9);
|
||||
let t10;
|
||||
if ($[15] !== connected || $[16] !== error || $[17] !== reconnecting || $[18] !== sessionActive) {
|
||||
t10 = getBridgeStatus({
|
||||
error,
|
||||
connected,
|
||||
sessionActive,
|
||||
reconnecting
|
||||
});
|
||||
$[15] = connected;
|
||||
$[16] = error;
|
||||
$[17] = reconnecting;
|
||||
$[18] = sessionActive;
|
||||
$[19] = t10;
|
||||
} else {
|
||||
t10 = $[19];
|
||||
}
|
||||
const {
|
||||
label: statusLabel,
|
||||
color: statusColor
|
||||
} = t10;
|
||||
const indicator = error ? BRIDGE_FAILED_INDICATOR : BRIDGE_READY_INDICATOR;
|
||||
let T0;
|
||||
let T1;
|
||||
let footerText;
|
||||
let t11;
|
||||
let t12;
|
||||
let t13;
|
||||
let t14;
|
||||
let t15;
|
||||
let t16;
|
||||
let t17;
|
||||
if ($[20] !== branchName || $[21] !== displayUrl || $[22] !== environmentId || $[23] !== error || $[24] !== indicator || $[25] !== onDone || $[26] !== qrText || $[27] !== sessionActive || $[28] !== sessionId || $[29] !== showQR || $[30] !== statusColor || $[31] !== statusLabel || $[32] !== verbose) {
|
||||
const qrLines = qrText ? qrText.split("\n").filter(_temp13) : [];
|
||||
let contextParts;
|
||||
if ($[43] !== branchName) {
|
||||
contextParts = [];
|
||||
if (repoName) {
|
||||
contextParts.push(repoName);
|
||||
}
|
||||
if (branchName) {
|
||||
contextParts.push(branchName);
|
||||
}
|
||||
$[43] = branchName;
|
||||
$[44] = contextParts;
|
||||
} else {
|
||||
contextParts = $[44];
|
||||
setAppState(prev => {
|
||||
if (!prev.replBridgeEnabled) return prev
|
||||
return { ...prev, replBridgeEnabled: false }
|
||||
})
|
||||
onDone()
|
||||
}
|
||||
const contextSuffix = contextParts.length > 0 ? " \xB7 " + contextParts.join(" \xB7 ") : "";
|
||||
let t18;
|
||||
if ($[45] !== displayUrl || $[46] !== error || $[47] !== sessionActive) {
|
||||
t18 = error ? FAILED_FOOTER_TEXT : displayUrl ? sessionActive ? buildActiveFooterText(displayUrl) : buildIdleFooterText(displayUrl) : undefined;
|
||||
$[45] = displayUrl;
|
||||
$[46] = error;
|
||||
$[47] = sessionActive;
|
||||
$[48] = t18;
|
||||
} else {
|
||||
t18 = $[48];
|
||||
}
|
||||
footerText = t18;
|
||||
T1 = Dialog;
|
||||
t15 = "Remote Control";
|
||||
t16 = onDone;
|
||||
t17 = true;
|
||||
T0 = Box;
|
||||
t11 = "column";
|
||||
t12 = 1;
|
||||
let t19;
|
||||
if ($[49] !== indicator || $[50] !== statusColor || $[51] !== statusLabel) {
|
||||
t19 = <Text color={statusColor}>{indicator} {statusLabel}</Text>;
|
||||
$[49] = indicator;
|
||||
$[50] = statusColor;
|
||||
$[51] = statusLabel;
|
||||
$[52] = t19;
|
||||
} else {
|
||||
t19 = $[52];
|
||||
}
|
||||
let t20;
|
||||
if ($[53] !== contextSuffix) {
|
||||
t20 = <Text dimColor={true}>{contextSuffix}</Text>;
|
||||
$[53] = contextSuffix;
|
||||
$[54] = t20;
|
||||
} else {
|
||||
t20 = $[54];
|
||||
}
|
||||
let t21;
|
||||
if ($[55] !== t19 || $[56] !== t20) {
|
||||
t21 = <Text>{t19}{t20}</Text>;
|
||||
$[55] = t19;
|
||||
$[56] = t20;
|
||||
$[57] = t21;
|
||||
} else {
|
||||
t21 = $[57];
|
||||
}
|
||||
let t22;
|
||||
if ($[58] !== error) {
|
||||
t22 = error && <Text color="error">{error}</Text>;
|
||||
$[58] = error;
|
||||
$[59] = t22;
|
||||
} else {
|
||||
t22 = $[59];
|
||||
}
|
||||
let t23;
|
||||
if ($[60] !== environmentId || $[61] !== verbose) {
|
||||
t23 = verbose && environmentId && <Text dimColor={true}>Environment: {environmentId}</Text>;
|
||||
$[60] = environmentId;
|
||||
$[61] = verbose;
|
||||
$[62] = t23;
|
||||
} else {
|
||||
t23 = $[62];
|
||||
}
|
||||
let t24;
|
||||
if ($[63] !== sessionId || $[64] !== verbose) {
|
||||
t24 = verbose && sessionId && <Text dimColor={true}>Session: {sessionId}</Text>;
|
||||
$[63] = sessionId;
|
||||
$[64] = verbose;
|
||||
$[65] = t24;
|
||||
} else {
|
||||
t24 = $[65];
|
||||
}
|
||||
if ($[66] !== t21 || $[67] !== t22 || $[68] !== t23 || $[69] !== t24) {
|
||||
t13 = <Box flexDirection="column">{t21}{t22}{t23}{t24}</Box>;
|
||||
$[66] = t21;
|
||||
$[67] = t22;
|
||||
$[68] = t23;
|
||||
$[69] = t24;
|
||||
$[70] = t13;
|
||||
} else {
|
||||
t13 = $[70];
|
||||
}
|
||||
t14 = showQR && qrLines.length > 0 && <Box flexDirection="column">{qrLines.map(_temp14)}</Box>;
|
||||
$[20] = branchName;
|
||||
$[21] = displayUrl;
|
||||
$[22] = environmentId;
|
||||
$[23] = error;
|
||||
$[24] = indicator;
|
||||
$[25] = onDone;
|
||||
$[26] = qrText;
|
||||
$[27] = sessionActive;
|
||||
$[28] = sessionId;
|
||||
$[29] = showQR;
|
||||
$[30] = statusColor;
|
||||
$[31] = statusLabel;
|
||||
$[32] = verbose;
|
||||
$[33] = T0;
|
||||
$[34] = T1;
|
||||
$[35] = footerText;
|
||||
$[36] = t11;
|
||||
$[37] = t12;
|
||||
$[38] = t13;
|
||||
$[39] = t14;
|
||||
$[40] = t15;
|
||||
$[41] = t16;
|
||||
$[42] = t17;
|
||||
} else {
|
||||
T0 = $[33];
|
||||
T1 = $[34];
|
||||
footerText = $[35];
|
||||
t11 = $[36];
|
||||
t12 = $[37];
|
||||
t13 = $[38];
|
||||
t14 = $[39];
|
||||
t15 = $[40];
|
||||
t16 = $[41];
|
||||
t17 = $[42];
|
||||
}
|
||||
let t18;
|
||||
if ($[71] !== footerText) {
|
||||
t18 = footerText && <Text dimColor={true}>{footerText}</Text>;
|
||||
$[71] = footerText;
|
||||
$[72] = t18;
|
||||
} else {
|
||||
t18 = $[72];
|
||||
}
|
||||
let t19;
|
||||
if ($[73] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t19 = <Text dimColor={true}>d to disconnect · space for QR code · Enter/Esc to close</Text>;
|
||||
$[73] = t19;
|
||||
} else {
|
||||
t19 = $[73];
|
||||
}
|
||||
let t20;
|
||||
if ($[74] !== T0 || $[75] !== t11 || $[76] !== t12 || $[77] !== t13 || $[78] !== t14 || $[79] !== t18) {
|
||||
t20 = <T0 flexDirection={t11} gap={t12}>{t13}{t14}{t18}{t19}</T0>;
|
||||
$[74] = T0;
|
||||
$[75] = t11;
|
||||
$[76] = t12;
|
||||
$[77] = t13;
|
||||
$[78] = t14;
|
||||
$[79] = t18;
|
||||
$[80] = t20;
|
||||
} else {
|
||||
t20 = $[80];
|
||||
}
|
||||
let t21;
|
||||
if ($[81] !== T1 || $[82] !== t15 || $[83] !== t16 || $[84] !== t17 || $[85] !== t20) {
|
||||
t21 = <T1 title={t15} onCancel={t16} hideInputGuide={t17}>{t20}</T1>;
|
||||
$[81] = T1;
|
||||
$[82] = t15;
|
||||
$[83] = t16;
|
||||
$[84] = t17;
|
||||
$[85] = t20;
|
||||
$[86] = t21;
|
||||
} else {
|
||||
t21 = $[86];
|
||||
}
|
||||
return t21;
|
||||
}
|
||||
function _temp14(line, i) {
|
||||
return <Text key={i}>{line}</Text>;
|
||||
}
|
||||
function _temp13(l) {
|
||||
return l.length > 0;
|
||||
}
|
||||
function _temp12(prev_0) {
|
||||
if (!prev_0.replBridgeEnabled) {
|
||||
return prev_0;
|
||||
}
|
||||
return {
|
||||
...prev_0,
|
||||
replBridgeEnabled: false
|
||||
};
|
||||
}
|
||||
function _temp11(current) {
|
||||
if (current.remoteControlAtStartup === false) {
|
||||
return current;
|
||||
}
|
||||
return {
|
||||
...current,
|
||||
remoteControlAtStartup: false
|
||||
};
|
||||
}
|
||||
function _temp10(prev) {
|
||||
return !prev;
|
||||
}
|
||||
function _temp1() {}
|
||||
function _temp0(s_8) {
|
||||
return s_8.verbose;
|
||||
}
|
||||
function _temp9(s_7) {
|
||||
return s_7.replBridgeSessionId;
|
||||
}
|
||||
function _temp8(s_6) {
|
||||
return s_6.replBridgeEnvironmentId;
|
||||
}
|
||||
function _temp7(s_5) {
|
||||
return s_5.replBridgeExplicit;
|
||||
}
|
||||
function _temp6(s_4) {
|
||||
return s_4.replBridgeError;
|
||||
}
|
||||
function _temp5(s_3) {
|
||||
return s_3.replBridgeSessionUrl;
|
||||
}
|
||||
function _temp4(s_2) {
|
||||
return s_2.replBridgeConnectUrl;
|
||||
}
|
||||
function _temp3(s_1) {
|
||||
return s_1.replBridgeReconnecting;
|
||||
}
|
||||
function _temp2(s_0) {
|
||||
return s_0.replBridgeSessionActive;
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.replBridgeConnected;
|
||||
})
|
||||
|
||||
const { label: statusLabel, color: statusColor } = getBridgeStatus({
|
||||
error,
|
||||
connected,
|
||||
sessionActive,
|
||||
reconnecting,
|
||||
})
|
||||
const indicator = error ? BRIDGE_FAILED_INDICATOR : BRIDGE_READY_INDICATOR
|
||||
const qrLines = qrText ? qrText.split('\n').filter(l => l.length > 0) : []
|
||||
|
||||
// Build suffix with repo and branch (matches standalone bridge format)
|
||||
const contextParts: string[] = []
|
||||
if (repoName) contextParts.push(repoName)
|
||||
if (branchName) contextParts.push(branchName)
|
||||
const contextSuffix =
|
||||
contextParts.length > 0 ? ' \u00b7 ' + contextParts.join(' \u00b7 ') : ''
|
||||
|
||||
// Footer text matches standalone bridge
|
||||
const footerText = error
|
||||
? FAILED_FOOTER_TEXT
|
||||
: displayUrl
|
||||
? sessionActive
|
||||
? buildActiveFooterText(displayUrl)
|
||||
: buildIdleFooterText(displayUrl)
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Dialog title="Remote Control" onCancel={onDone} hideInputGuide>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box flexDirection="column">
|
||||
<Text>
|
||||
<Text color={statusColor}>
|
||||
{indicator} {statusLabel}
|
||||
</Text>
|
||||
<Text dimColor>{contextSuffix}</Text>
|
||||
</Text>
|
||||
{error && <Text color="error">{error}</Text>}
|
||||
{verbose && environmentId && (
|
||||
<Text dimColor>Environment: {environmentId}</Text>
|
||||
)}
|
||||
{verbose && sessionId && <Text dimColor>Session: {sessionId}</Text>}
|
||||
</Box>
|
||||
{showQR && qrLines.length > 0 && (
|
||||
<Box flexDirection="column">
|
||||
{qrLines.map((line, i) => (
|
||||
<Text key={i}>{line}</Text>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
{footerText && <Text dimColor>{footerText}</Text>}
|
||||
<Text dimColor>
|
||||
d to disconnect · space for QR code · Enter/Esc to close
|
||||
</Text>
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,86 +1,73 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useCallback } from 'react';
|
||||
import { logEvent } from 'src/services/analytics/index.js';
|
||||
import { Box, Link, Newline, Text } from '../ink.js';
|
||||
import { gracefulShutdownSync } from '../utils/gracefulShutdown.js';
|
||||
import { updateSettingsForSource } from '../utils/settings/settings.js';
|
||||
import { Select } from './CustomSelect/index.js';
|
||||
import { Dialog } from './design-system/Dialog.js';
|
||||
import React, { useCallback } from 'react'
|
||||
import { logEvent } from 'src/services/analytics/index.js'
|
||||
import { Box, Link, Newline, Text } from '../ink.js'
|
||||
import { gracefulShutdownSync } from '../utils/gracefulShutdown.js'
|
||||
import { updateSettingsForSource } from '../utils/settings/settings.js'
|
||||
import { Select } from './CustomSelect/index.js'
|
||||
import { Dialog } from './design-system/Dialog.js'
|
||||
|
||||
type Props = {
|
||||
onAccept(): void;
|
||||
};
|
||||
export function BypassPermissionsModeDialog(t0) {
|
||||
const $ = _c(7);
|
||||
const {
|
||||
onAccept
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = [];
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
React.useEffect(_temp, t1);
|
||||
let t2;
|
||||
if ($[1] !== onAccept) {
|
||||
t2 = function onChange(value) {
|
||||
bb3: switch (value) {
|
||||
case "accept":
|
||||
{
|
||||
logEvent("tengu_bypass_permissions_mode_dialog_accept", {});
|
||||
updateSettingsForSource("userSettings", {
|
||||
skipDangerousModePermissionPrompt: true
|
||||
});
|
||||
onAccept();
|
||||
break bb3;
|
||||
}
|
||||
case "decline":
|
||||
{
|
||||
gracefulShutdownSync(1);
|
||||
}
|
||||
onAccept(): void
|
||||
}
|
||||
|
||||
export function BypassPermissionsModeDialog({
|
||||
onAccept,
|
||||
}: Props): React.ReactNode {
|
||||
React.useEffect(() => {
|
||||
logEvent('tengu_bypass_permissions_mode_dialog_shown', {})
|
||||
}, [])
|
||||
|
||||
function onChange(value: 'accept' | 'decline') {
|
||||
switch (value) {
|
||||
case 'accept': {
|
||||
logEvent('tengu_bypass_permissions_mode_dialog_accept', {})
|
||||
|
||||
updateSettingsForSource('userSettings', {
|
||||
skipDangerousModePermissionPrompt: true,
|
||||
})
|
||||
onAccept()
|
||||
break
|
||||
}
|
||||
};
|
||||
$[1] = onAccept;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
case 'decline': {
|
||||
gracefulShutdownSync(1)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
const onChange = t2;
|
||||
const handleEscape = _temp2;
|
||||
let t3;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Box flexDirection="column" gap={1}><Text>In Bypass Permissions mode, Claude Code will not ask for your approval before running potentially dangerous commands.<Newline />This mode should only be used in a sandboxed container/VM that has restricted internet access and can easily be restored if damaged.</Text><Text>By proceeding, you accept all responsibility for actions taken while running in Bypass Permissions mode.</Text><Link url="https://code.claude.com/docs/en/security" /></Box>;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
let t4;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = [{
|
||||
label: "No, exit",
|
||||
value: "decline"
|
||||
}, {
|
||||
label: "Yes, I accept",
|
||||
value: "accept"
|
||||
}];
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
let t5;
|
||||
if ($[5] !== onChange) {
|
||||
t5 = <Dialog title="WARNING: Claude Code running in Bypass Permissions mode" color="error" onCancel={handleEscape}>{t3}<Select options={t4} onChange={value_0 => onChange(value_0 as 'accept' | 'decline')} /></Dialog>;
|
||||
$[5] = onChange;
|
||||
$[6] = t5;
|
||||
} else {
|
||||
t5 = $[6];
|
||||
}
|
||||
return t5;
|
||||
}
|
||||
function _temp2() {
|
||||
gracefulShutdownSync(0);
|
||||
}
|
||||
function _temp() {
|
||||
logEvent("tengu_bypass_permissions_mode_dialog_shown", {});
|
||||
|
||||
const handleEscape = useCallback(() => {
|
||||
gracefulShutdownSync(0)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="WARNING: Claude Code running in Bypass Permissions mode"
|
||||
color="error"
|
||||
onCancel={handleEscape}
|
||||
>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text>
|
||||
In Bypass Permissions mode, Claude Code will not ask for your approval
|
||||
before running potentially dangerous commands.
|
||||
<Newline />
|
||||
This mode should only be used in a sandboxed container/VM that has
|
||||
restricted internet access and can easily be restored if damaged.
|
||||
</Text>
|
||||
<Text>
|
||||
By proceeding, you accept all responsibility for actions taken while
|
||||
running in Bypass Permissions mode.
|
||||
</Text>
|
||||
|
||||
<Link url="https://code.claude.com/docs/en/security" />
|
||||
</Box>
|
||||
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'No, exit', value: 'decline' },
|
||||
{ label: 'Yes, I accept', value: 'accept' },
|
||||
]}
|
||||
onChange={value => onChange(value as 'accept' | 'decline')}
|
||||
/>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user