refactor: jsc-mod 改名 jsc-mod-statusline、jsc-companion 改名 jsc-mod-companion,指令拆成獨立指令
- jsc-mod-statusline:/jsc-mod-statusline:enable、compact、disable - jsc-mod-companion:/jsc-mod-companion:status、switch、disable、setup、todo、quiet - 同步更新 plugin.json、marketplace.json 與各 README - 補上 statusline band 測試缺少的 ui.render 底層處理 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
0e7636c185
commit
ea17f1cb2d
@@ -0,0 +1,120 @@
|
||||
import type { Register } from 'claude-code'
|
||||
|
||||
import { fitCompanion } from './companion/band'
|
||||
import { createCompanion } from './companion/core'
|
||||
import { engineFs } from './companion/engine-fs'
|
||||
|
||||
let companion: ReturnType<typeof createCompanion> | undefined
|
||||
let replying = false
|
||||
let usedTools = false
|
||||
let typedAt = 0
|
||||
let timer: { cancel(): void } | undefined
|
||||
|
||||
const TICK_MS = 60_000
|
||||
const TYPING_MS = 5_000
|
||||
|
||||
const COMMANDS: Record<string, { description: string; argumentHint?: string }> = {
|
||||
status: { description: '查看目前角色、心情、好感與待辦數量' },
|
||||
switch: { description: '切換角色;不給角色會列出可用角色', argumentHint: '<角色>' },
|
||||
disable: { description: '關閉陪伴,所有資料保留' },
|
||||
setup: { description: '顯示或儲存使用者設定', argumentHint: '[key=value…]' },
|
||||
todo: { description: '待辦:列出、新增、記錄承諾、完成', argumentHint: '[list | add | promise | done] …' },
|
||||
quiet: { description: '暫時靜音,不主動關心', argumentHint: '[分鐘]' },
|
||||
}
|
||||
|
||||
export const register: Register = on => {
|
||||
on('session.start', async ($, e, next) => {
|
||||
for (const [name, c] of Object.entries(COMMANDS)) await $.command.register({ name, ...c })
|
||||
const home = await $.env.get('HOME')
|
||||
companion = createCompanion({ fs: engineFs({ read: p => $.fs.read(p), write: (p, t) => $.fs.write(p, t), list: p => $.fs.list(p) }), clock: { now: Date.now }, root: `${home ?? ''}/.companion` })
|
||||
const first = await companion.ensureActivated()
|
||||
if (first.first) $.ui.toast(`已啟用 ${first.name}。用 /jsc-mod-companion:disable 可關閉`)
|
||||
|
||||
// 定時檢查 toast 類關心(到期待辦、作息);失敗不影響主流程
|
||||
timer?.cancel()
|
||||
timer = $.clock.every(TICK_MS, () => {
|
||||
companion
|
||||
?.tick({ replying, typing: Date.now() - typedAt < TYPING_MS })
|
||||
.then(cares => cares.forEach(c => $.ui.toast(c.text)))
|
||||
.catch(() => {})
|
||||
})
|
||||
|
||||
return next(e)
|
||||
})
|
||||
|
||||
for (const command of Object.keys(COMMANDS))
|
||||
on('command.run', { command }, async ($, e) => ({
|
||||
text: companion ? await companion.command(`${command === 'disable' ? 'off' : command} ${e.args}`.trim()) : '尚未就緒。',
|
||||
}))
|
||||
|
||||
// 角色卡附加在 system prompt 尾端(session 區段),語氣才會進到模型
|
||||
on('prompt.compose', async ($, e, next) => {
|
||||
const result = await next(e)
|
||||
const text = await companion?.prompt()
|
||||
if (!text) return result
|
||||
return { sections: [...result.sections, { id: 'companion:persona', text, scope: 'session' as const }] }
|
||||
})
|
||||
|
||||
on('prompt.submit', async ($, e, next) => {
|
||||
// 更新心情失敗不能擋下使用者的提示
|
||||
const care = (await companion?.onPrompt(e.text).catch(() => [] as string[])) ?? []
|
||||
return next(care.length ? { ...e, context: [...(e.context ?? []), ...care] } : e)
|
||||
})
|
||||
|
||||
on('prompt.edit', async ($, e, next) => {
|
||||
typedAt = Date.now()
|
||||
return next(e)
|
||||
})
|
||||
|
||||
on('turn.start', async ($, e, next) => {
|
||||
replying = true
|
||||
usedTools = false
|
||||
return next(e)
|
||||
})
|
||||
|
||||
on('turn.complete', async ($, e, next) => {
|
||||
replying = false
|
||||
// 只有主迴圈正常答完、且這輪用過工具,才算做完一件事
|
||||
if (e.reason === 'answer' && !e.agentId) companion?.onTurnComplete(usedTools).catch(() => {})
|
||||
return next(e)
|
||||
})
|
||||
|
||||
// 連續工具失敗會觸發關心,並影響心情
|
||||
on('tool.call', async ($, e, next) => {
|
||||
const r = await next(e)
|
||||
if (!e.agentId) usedTools = true
|
||||
companion?.onToolResult(!r.isError, { command: e.tool === 'Bash' ? e.command : undefined }).catch(() => {})
|
||||
return r
|
||||
})
|
||||
|
||||
on('session.end', async ($, e, next) => {
|
||||
timer?.cancel()
|
||||
timer = undefined
|
||||
await companion?.end().catch(() => {})
|
||||
return next(e)
|
||||
})
|
||||
|
||||
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
|
||||
const rest = await next(e)
|
||||
const view = await companion?.view()
|
||||
if (e.props.hasSurvey || !view) return rest
|
||||
|
||||
const line = fitCompanion(view, e.props.bodyColumns)
|
||||
const { Box, Text } = $.ui.resolve(e)
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{rest}
|
||||
<Box marginTop={1}>
|
||||
<Text wrap="truncate">
|
||||
{line.map(s => (
|
||||
<Text color={s.color} dimColor={s.dim} bold={s.bold}>
|
||||
{s.text}
|
||||
</Text>
|
||||
))}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user