@cocorograph/hub-agent 0.6.61 → 0.6.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/claude-history.mjs +42 -6
- package/src/main.mjs +51 -7
- package/src/tmux.mjs +78 -6
package/package.json
CHANGED
package/src/claude-history.mjs
CHANGED
|
@@ -148,11 +148,47 @@ export async function fetchSessionHistory({ cwd, session_id, maxLines, projectsR
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
/**
|
|
151
|
-
*
|
|
152
|
-
*
|
|
151
|
+
* user メッセージ本文を preview 用テキストへ正規化する。実発言ではない合成メッセージや
|
|
152
|
+
* コンテキスト操作コマンド (/clear・/compact) の痕跡をスキップし、業務スラッシュコマンドは
|
|
153
|
+
* `/name args` ラベルへ整形する。frontend hooks/cockpit/stream/events.ts の
|
|
154
|
+
* isHiddenUserEvent / slashCommandLabel と整合させる (別リポのため同等ロジックを再実装)。
|
|
155
|
+
*
|
|
156
|
+
* @param {string} raw
|
|
157
|
+
* @returns {string|null} preview に採用するテキスト (最大 80 文字)。スキップすべき行なら null。
|
|
158
|
+
*/
|
|
159
|
+
function normalizePreviewText(raw) {
|
|
160
|
+
const trimmed = (raw || "").trim()
|
|
161
|
+
if (!trimmed) return null
|
|
162
|
+
// SDK / ハーネスが注入する合成 user メッセージはスキップ (会話の主題ではない)。
|
|
163
|
+
if (trimmed.startsWith("[Request interrupted by user")) return null
|
|
164
|
+
if (trimmed.startsWith("Continue from where you left off")) return null
|
|
165
|
+
if (trimmed.startsWith("Base directory for this skill:")) return null
|
|
166
|
+
// スラッシュコマンド echo (<command-name> 注入) → `/name args` へ正規化。
|
|
167
|
+
// /clear・/compact は session 回転 / 圧縮の痕跡で会話本文ではないため preview から除外し、
|
|
168
|
+
// 次の実発言を拾う ("一覧が全部 /clear で始まる" 問題の解消)。
|
|
169
|
+
const nameMatch = trimmed.match(/<command-name>([^<]*)<\/command-name>/)
|
|
170
|
+
if (nameMatch) {
|
|
171
|
+
const name = nameMatch[1].trim()
|
|
172
|
+
if (name === "/clear" || name === "/compact") return null
|
|
173
|
+
const argsMatch = trimmed.match(/<command-args>([\s\S]*?)<\/command-args>/)
|
|
174
|
+
const args = (argsMatch?.[1] || "").trim()
|
|
175
|
+
const label = args ? `${name} ${args}` : name
|
|
176
|
+
return label.replace(/\s+/g, " ").slice(0, 80)
|
|
177
|
+
}
|
|
178
|
+
// local-command の stdout / caveat だけの注入行はスキップ。
|
|
179
|
+
if (trimmed.includes("<local-command-stdout>") || trimmed.includes("<local-command-caveat>")) {
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
return trimmed.replace(/\s+/g, " ").slice(0, 80)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* jsonl ファイルから preview 用テキストを抽出する。最初の「実ユーザー発言」(合成メッセージ /
|
|
187
|
+
* /clear・/compact の痕跡を除く) を返す。大きいファイルでも先頭付近で見つかるので、
|
|
188
|
+
* 先頭 64KB だけ読んで探す。
|
|
153
189
|
*
|
|
154
190
|
* @param {string} filePath
|
|
155
|
-
* @returns {Promise<string>}
|
|
191
|
+
* @returns {Promise<string>} preview テキスト (最大 80 文字)、無ければ ""
|
|
156
192
|
*/
|
|
157
193
|
async function extractPreview(filePath) {
|
|
158
194
|
// P-perf: ファイル全体を readFile してから 64KB に slice すると、肥大化した jsonl で
|
|
@@ -180,7 +216,7 @@ async function extractPreview(filePath) {
|
|
|
180
216
|
} catch {
|
|
181
217
|
continue
|
|
182
218
|
}
|
|
183
|
-
if (obj?.type === "user" && obj.message) {
|
|
219
|
+
if (obj?.type === "user" && obj.message && obj.isMeta !== true) {
|
|
184
220
|
const content = obj.message.content
|
|
185
221
|
let str = ""
|
|
186
222
|
if (typeof content === "string") {
|
|
@@ -189,8 +225,8 @@ async function extractPreview(filePath) {
|
|
|
189
225
|
const textBlock = content.find((b) => b?.type === "text" && typeof b.text === "string")
|
|
190
226
|
if (textBlock) str = textBlock.text
|
|
191
227
|
}
|
|
192
|
-
|
|
193
|
-
if (
|
|
228
|
+
const preview = normalizePreviewText(str)
|
|
229
|
+
if (preview) return preview
|
|
194
230
|
}
|
|
195
231
|
}
|
|
196
232
|
return ""
|
package/src/main.mjs
CHANGED
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
removeWorktree as removeWorktreeDir,
|
|
70
70
|
resumeWithMessage,
|
|
71
71
|
setTmuxGlobalEnv,
|
|
72
|
+
setTuiEffort,
|
|
72
73
|
setTuiModel,
|
|
73
74
|
} from "./tmux.mjs"
|
|
74
75
|
import { TuiPermissionBridge } from "./tui-permission-bridge.mjs"
|
|
@@ -1419,6 +1420,39 @@ async function dispatch(msg, ctx) {
|
|
|
1419
1420
|
})()
|
|
1420
1421
|
return
|
|
1421
1422
|
}
|
|
1423
|
+
case "claude.tui.setEffort": {
|
|
1424
|
+
// effort バッジ選択 → 対話 claude TUI へ `/effort <level>` を送って reasoning effort を
|
|
1425
|
+
// 切り替える。setModel と同設計: agent が実キーを送出 → 全ブラウザへ claude.tui.effort を
|
|
1426
|
+
// broadcast し、実際に動いているターミナルの effort を正本として全端末に同期する。
|
|
1427
|
+
// effort は jsonl に記録されないため、この即時 broadcast が唯一の表示根拠になる。
|
|
1428
|
+
const cwd = typeof msg.cwd === "string" ? msg.cwd : ""
|
|
1429
|
+
const sessionName =
|
|
1430
|
+
typeof msg.session_name === "string" ? msg.session_name : ""
|
|
1431
|
+
if (!sessionName) return
|
|
1432
|
+
// effort="" は「auto」= `/effort auto`。frontend へはそのまま空で返し、バッジは auto 表示。
|
|
1433
|
+
const effort = typeof msg.effort === "string" ? msg.effort : ""
|
|
1434
|
+
;(async () => {
|
|
1435
|
+
try {
|
|
1436
|
+
await setTuiEffort(sessionName, effort || "auto", { logger })
|
|
1437
|
+
ctx.client.send({
|
|
1438
|
+
type: "claude.tui.effort",
|
|
1439
|
+
cwd: cwd || undefined,
|
|
1440
|
+
session_name: sessionName,
|
|
1441
|
+
effort,
|
|
1442
|
+
})
|
|
1443
|
+
logger.info(
|
|
1444
|
+
{ session: sessionName, cwd, effort: effort || "(auto)" },
|
|
1445
|
+
"tui effort switched → notified browser",
|
|
1446
|
+
)
|
|
1447
|
+
} catch (err) {
|
|
1448
|
+
logger.warn(
|
|
1449
|
+
{ err: err?.message, session: sessionName },
|
|
1450
|
+
"claude.tui.setEffort failed",
|
|
1451
|
+
)
|
|
1452
|
+
}
|
|
1453
|
+
})()
|
|
1454
|
+
return
|
|
1455
|
+
}
|
|
1422
1456
|
case "claude.tui.probePermission": {
|
|
1423
1457
|
// 読み取り専用の権限モード問い合わせ (cold-load seed)。キーは送らず、ペインを
|
|
1424
1458
|
// 読んで現在の実モードを claude.tui.permission として broadcast するだけ。
|
|
@@ -1528,12 +1562,16 @@ async function dispatch(msg, ctx) {
|
|
|
1528
1562
|
...payload,
|
|
1529
1563
|
})
|
|
1530
1564
|
try {
|
|
1531
|
-
//
|
|
1565
|
+
// fresh: 「まっさらな新規セッション」要求 (ピッカーの「+新規」)。cwd 最新へ
|
|
1566
|
+
// resume せず resume 無しの claude を起動する。session_id は起動後の初回送信で
|
|
1567
|
+
// 生成され、frontend は回転検知 / sessions 応答で拾う。
|
|
1568
|
+
const fresh = msg.fresh === true
|
|
1569
|
+
// 1) 載せ替え先 session_id を確定 (fresh は解決しない = 新規起動)。
|
|
1532
1570
|
let targetId =
|
|
1533
1571
|
typeof msg.session_id === "string" && msg.session_id
|
|
1534
1572
|
? msg.session_id
|
|
1535
1573
|
: null
|
|
1536
|
-
if (!targetId && cwd) {
|
|
1574
|
+
if (!fresh && !targetId && cwd) {
|
|
1537
1575
|
const projectsRoot = await getActiveProjectsRoot()
|
|
1538
1576
|
const { sessions } = await listSessions({
|
|
1539
1577
|
cwd,
|
|
@@ -1551,26 +1589,32 @@ async function dispatch(msg, ctx) {
|
|
|
1551
1589
|
session_id: targetId || undefined,
|
|
1552
1590
|
})
|
|
1553
1591
|
}
|
|
1554
|
-
// 3) tmux claude
|
|
1555
|
-
// 冪等性: 同じ session
|
|
1592
|
+
// 3) tmux claude を起動し直す (resume or fresh)。
|
|
1593
|
+
// 冪等性: 同じ session を同じキーへ既に載せ替え済みなら respawn しない
|
|
1556
1594
|
// (TUI ビューは remount のたびに bind を送るため、毎回 respawn すると
|
|
1557
1595
|
// claude が再起動ループになり送信不能になる。2026-06-06 leader 実機)。
|
|
1596
|
+
// resume は targetId をキー、fresh は session_id 不在のため bind の request_id を
|
|
1597
|
+
// キーにする (同一マウントの再送は同 request_id で skip、新規要求は再マウントで
|
|
1598
|
+
// 新 request_id になり 1 回だけ起動する)。
|
|
1558
1599
|
let rebind = { ok: false, skipped: false }
|
|
1559
|
-
|
|
1560
|
-
|
|
1600
|
+
const bindKey = fresh ? `fresh:${request_id || ""}` : targetId
|
|
1601
|
+
if (bindKey && sessionName && (fresh || targetId)) {
|
|
1602
|
+
if (ctx.tuiReboundSessions.get(sessionName) === bindKey) {
|
|
1561
1603
|
rebind = { ok: true, skipped: true }
|
|
1562
1604
|
} else {
|
|
1563
1605
|
rebind = await rebindClaudeSession(sessionName, targetId, {
|
|
1564
1606
|
cwd,
|
|
1565
1607
|
model: ctx.config?.claude_model || "",
|
|
1566
1608
|
permissionMode: ctx.config?.claude_permission_mode || "",
|
|
1609
|
+
fresh,
|
|
1567
1610
|
logger,
|
|
1568
1611
|
})
|
|
1569
|
-
if (rebind.ok) ctx.tuiReboundSessions.set(sessionName,
|
|
1612
|
+
if (rebind.ok) ctx.tuiReboundSessions.set(sessionName, bindKey)
|
|
1570
1613
|
}
|
|
1571
1614
|
}
|
|
1572
1615
|
reply({
|
|
1573
1616
|
session_id: targetId,
|
|
1617
|
+
fresh,
|
|
1574
1618
|
stopped_sdk: stoppedSdk,
|
|
1575
1619
|
rebound: !!rebind.ok,
|
|
1576
1620
|
skipped: !!rebind.skipped,
|
package/src/tmux.mjs
CHANGED
|
@@ -770,6 +770,19 @@ export function buildResumeCmd(sessionId, opts = {}) {
|
|
|
770
770
|
return `claude --resume ${sessionId}${flags ? " " + flags : ""}`.trim()
|
|
771
771
|
}
|
|
772
772
|
|
|
773
|
+
/**
|
|
774
|
+
* まっさらな新規セッション起動用の `claude [flags]` コマンド文字列を組み立てる (純粋関数)。
|
|
775
|
+
* --resume を付けないので claude は新しい session_id (新 jsonl) で起動する。Cockpit の
|
|
776
|
+
* セッション選択プルダウンの「+新規」で、cwd 最新へ resume せず仕切り直したいときに使う。
|
|
777
|
+
*
|
|
778
|
+
* @param {{model?:string,permissionMode?:string}} [opts]
|
|
779
|
+
* @returns {string}
|
|
780
|
+
*/
|
|
781
|
+
export function buildFreshCmd(opts = {}) {
|
|
782
|
+
const flags = composeClaudeFlags(opts)
|
|
783
|
+
return `claude${flags ? " " + flags : ""}`.trim()
|
|
784
|
+
}
|
|
785
|
+
|
|
773
786
|
const _delay = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
774
787
|
|
|
775
788
|
/**
|
|
@@ -921,6 +934,59 @@ export async function setTuiModel(name, modelArg, opts = {}) {
|
|
|
921
934
|
}
|
|
922
935
|
}
|
|
923
936
|
|
|
937
|
+
/**
|
|
938
|
+
* 対話 claude TUI に `/effort <level>` を送って reasoning effort を切り替える。
|
|
939
|
+
*
|
|
940
|
+
* cockpit の effort バッジ選択 (claude.tui.setEffort) の書込側。モデル切替 (setTuiModel) と
|
|
941
|
+
* 同設計で、frontend は raw pty.data ではなく claude.tui.setEffort を送り、agent 側で本関数を
|
|
942
|
+
* 実行 → 全ブラウザへ claude.tui.effort を broadcast して全端末を実 effort に揃える。
|
|
943
|
+
*
|
|
944
|
+
* `/effort` は引数 (low/medium/high/xhigh/max/auto) を受理し即時反映する (確認プロンプトを
|
|
945
|
+
* 挟まない)。effort 非対応レベルを与えても CLI がモデルの対応上限へ自動フォールバックする。
|
|
946
|
+
* `auto` はモデル既定へリセット。copy-mode に入っているとキーが奪われるので先に抜ける。
|
|
947
|
+
* ベストエフォート。
|
|
948
|
+
*
|
|
949
|
+
* @param {string} name tmux セッション名
|
|
950
|
+
* @param {string} effortArg `/effort` 引数 (low/medium/high/xhigh/max または "auto")
|
|
951
|
+
* @param {{logger?:object,tmuxBin?:string}} [opts]
|
|
952
|
+
* @returns {Promise<{ok:boolean, error?:string}>}
|
|
953
|
+
*/
|
|
954
|
+
export async function setTuiEffort(name, effortArg, opts = {}) {
|
|
955
|
+
const bin = tmuxBin(opts)
|
|
956
|
+
const arg = String(effortArg || "auto").replace(/[\r\n]+/g, " ").trim()
|
|
957
|
+
if (!arg) return { ok: false, error: "empty effort arg" }
|
|
958
|
+
try {
|
|
959
|
+
// copy-mode 等に入っているとキーが奪われるので、入っている時だけ抜ける。
|
|
960
|
+
try {
|
|
961
|
+
const { stdout } = await execFileP(bin, [
|
|
962
|
+
"display-message",
|
|
963
|
+
"-p",
|
|
964
|
+
"-t",
|
|
965
|
+
`${name}:`,
|
|
966
|
+
"-F",
|
|
967
|
+
"#{pane_in_mode}",
|
|
968
|
+
])
|
|
969
|
+
if (stdout.trim() === "1") {
|
|
970
|
+
await execFileP(bin, ["send-keys", "-t", name, "-X", "cancel"])
|
|
971
|
+
}
|
|
972
|
+
} catch {
|
|
973
|
+
// pane_in_mode 取得失敗はベストエフォートで無視。
|
|
974
|
+
}
|
|
975
|
+
// `/effort <level>` をリテラルで送る (-l でキー名解釈・スラッシュ補完の暴発を避ける)。
|
|
976
|
+
await execFileP(bin, ["send-keys", "-t", name, "-l", `/effort ${arg}`])
|
|
977
|
+
await _delay(120)
|
|
978
|
+
// Enter で本文確定 (send-keys の離散イベントなので paste 吸収は起きにくい)。
|
|
979
|
+
await execFileP(bin, ["send-keys", "-t", name, "Enter"])
|
|
980
|
+
return { ok: true }
|
|
981
|
+
} catch (err) {
|
|
982
|
+
opts.logger?.warn(
|
|
983
|
+
{ session: name, effort: arg, err: err?.message },
|
|
984
|
+
"setTuiEffort failed",
|
|
985
|
+
)
|
|
986
|
+
return { ok: false, error: err?.message || String(err) }
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
924
990
|
/**
|
|
925
991
|
* 中断キャンセル後の入力欄復旧 (claude.tui.recoverInput / agent >= 0.6.57)。
|
|
926
992
|
*
|
|
@@ -1019,15 +1085,19 @@ export async function recoverTuiInput(name, expectText, opts = {}) {
|
|
|
1019
1085
|
*
|
|
1020
1086
|
* ベストエフォート。失敗しても throw せず {ok:false} を返し、上位は newest 表示に degrade する。
|
|
1021
1087
|
*
|
|
1088
|
+
* opts.fresh=true のときは sessionId を無視し、resume 無しの `claude` を起動して
|
|
1089
|
+
* まっさらな新規セッションにする (ピッカーの「+新規」)。
|
|
1090
|
+
*
|
|
1022
1091
|
* @param {string} name tmux セッション名
|
|
1023
|
-
* @param {string} sessionId 載せ替え先 Claude session_id
|
|
1024
|
-
* @param {{cwd?:string,model?:string,permissionMode?:string,logger?:object,tmuxBin?:string}} [opts]
|
|
1092
|
+
* @param {string|null} sessionId 載せ替え先 Claude session_id (opts.fresh 時は null 可)
|
|
1093
|
+
* @param {{cwd?:string,model?:string,permissionMode?:string,fresh?:boolean,logger?:object,tmuxBin?:string}} [opts]
|
|
1025
1094
|
* @returns {Promise<{ok:boolean, cmd?:string, error?:string}>}
|
|
1026
1095
|
*/
|
|
1027
1096
|
export async function rebindClaudeSession(name, sessionId, opts = {}) {
|
|
1028
1097
|
let cmd
|
|
1029
1098
|
try {
|
|
1030
|
-
|
|
1099
|
+
// fresh=新規起動 (--resume 無し)。それ以外は session_id 検証込みの resume。
|
|
1100
|
+
cmd = opts.fresh ? buildFreshCmd(opts) : buildResumeCmd(sessionId, opts)
|
|
1031
1101
|
} catch (err) {
|
|
1032
1102
|
return { ok: false, error: err?.message || String(err) }
|
|
1033
1103
|
}
|
|
@@ -1040,11 +1110,13 @@ export async function rebindClaudeSession(name, sessionId, opts = {}) {
|
|
|
1040
1110
|
await execFileP(bin, respArgs)
|
|
1041
1111
|
// 2) シェル準備待ち (rc 読み込み等)。send-keys は pty にバッファされるので過敏でなくてよい。
|
|
1042
1112
|
await _delay(300)
|
|
1043
|
-
// 3) クリーンなシェルに resume
|
|
1113
|
+
// 3) クリーンなシェルに claude 起動コマンドを送る (resume or 新規)。
|
|
1044
1114
|
await execFileP(bin, ["send-keys", "-t", name, cmd, "Enter"])
|
|
1045
1115
|
opts.logger?.info(
|
|
1046
|
-
{ session: name, resume: sessionId },
|
|
1047
|
-
|
|
1116
|
+
{ session: name, resume: opts.fresh ? null : sessionId, fresh: !!opts.fresh },
|
|
1117
|
+
opts.fresh
|
|
1118
|
+
? "tui rebind: respawned pane shell + claude (fresh session)"
|
|
1119
|
+
: "tui rebind: respawned pane shell + claude --resume",
|
|
1048
1120
|
)
|
|
1049
1121
|
return { ok: true, cmd }
|
|
1050
1122
|
} catch (err) {
|