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
@@ -1,83 +0,0 @@
|
||||
# jsc-companion
|
||||
|
||||
## 指令
|
||||
|
||||
### `/jsc-companion:companion status`
|
||||
|
||||
查看目前角色、心情、好感與待辦數量。
|
||||
|
||||
```text
|
||||
/jsc-companion:companion status
|
||||
```
|
||||
|
||||
### `/jsc-companion:companion switch <角色>`
|
||||
|
||||
切換角色;不給角色會列出可用角色。
|
||||
|
||||
```text
|
||||
/jsc-companion:companion switch 小明
|
||||
```
|
||||
|
||||
### `/jsc-companion:companion off`
|
||||
|
||||
關閉陪伴,所有資料保留。
|
||||
|
||||
```text
|
||||
/jsc-companion:companion off
|
||||
```
|
||||
|
||||
### `/jsc-companion:companion setup [key=value…]`
|
||||
|
||||
不給參數時顯示目前設定;給 key=value 則儲存。可設定:`nickname`、`wake=HH:MM`、`sleep=HH:MM`、`tz=+0800`、`careLimit=N`(每日主動關心上限)、`quietStart=HH:MM`、`quietEnd=HH:MM`。
|
||||
|
||||
```text
|
||||
/jsc-companion:companion setup nickname=阿明 wake=07:30 sleep=23:30 tz=+0800 careLimit=3 quietStart=22:00 quietEnd=08:00
|
||||
```
|
||||
|
||||
### `/jsc-companion:companion todo [list]`
|
||||
|
||||
列出未完成的待辦。
|
||||
|
||||
```text
|
||||
/jsc-companion:companion todo
|
||||
```
|
||||
|
||||
### `/jsc-companion:companion todo add <內容> [due=YYYY-MM-DDTHH:MM]`
|
||||
|
||||
新增待辦,可指定到期時間(依 `tz` 設定換算)。
|
||||
|
||||
```text
|
||||
/jsc-companion:companion todo add 買牛奶 due=2026-10-15T18:00
|
||||
```
|
||||
|
||||
### `/jsc-companion:companion todo promise <內容> [due=YYYY-MM-DDTHH:MM]`
|
||||
|
||||
記錄角色答應使用者的事(列表中標示【承諾】),用法同 `todo add`。
|
||||
|
||||
```text
|
||||
/jsc-companion:companion todo promise 週末一起討論新章節 due=2026-10-17T10:00
|
||||
```
|
||||
|
||||
### `/jsc-companion:companion todo done <編號>`
|
||||
|
||||
將待辦標為完成。
|
||||
|
||||
```text
|
||||
/jsc-companion:companion todo done 1
|
||||
```
|
||||
|
||||
### `/jsc-companion:companion quiet [分鐘]`
|
||||
|
||||
暫時靜音,不主動關心;預設 60 分鐘,`0` 取消靜音。
|
||||
|
||||
```text
|
||||
/jsc-companion:companion quiet 30
|
||||
```
|
||||
|
||||
### `/jsc-companion:ingest`
|
||||
|
||||
解析單章小說(txt/md),逐一詢問合併、新建或略過後寫入角色資料;提供章節檔或要求建立角色時也會自動使用。
|
||||
|
||||
```text
|
||||
/jsc-companion:ingest chapter1.md 章節編號 1
|
||||
```
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "jsc-companion",
|
||||
"name": "jsc-mod-companion",
|
||||
"version": "0.0.3",
|
||||
"description": "小說角色陪伴:人設注入、語氣、心情與好感、互動記憶、主動關心與待辦,並可用 ingest skill 解析小說建立角色",
|
||||
"license": "MIT",
|
||||
@@ -0,0 +1,83 @@
|
||||
# jsc-mod-companion
|
||||
|
||||
## 指令
|
||||
|
||||
### `/jsc-mod-companion:status`
|
||||
|
||||
查看目前角色、心情、好感與待辦數量。
|
||||
|
||||
```text
|
||||
/jsc-mod-companion:status
|
||||
```
|
||||
|
||||
### `/jsc-mod-companion:switch <角色>`
|
||||
|
||||
切換角色;不給角色會列出可用角色。
|
||||
|
||||
```text
|
||||
/jsc-mod-companion:switch 小明
|
||||
```
|
||||
|
||||
### `/jsc-mod-companion:disable`
|
||||
|
||||
關閉陪伴,所有資料保留。
|
||||
|
||||
```text
|
||||
/jsc-mod-companion:disable
|
||||
```
|
||||
|
||||
### `/jsc-mod-companion:setup [key=value…]`
|
||||
|
||||
不給參數時顯示目前設定;給 key=value 則儲存。可設定:`nickname`、`wake=HH:MM`、`sleep=HH:MM`、`tz=+0800`、`careLimit=N`(每日主動關心上限)、`quietStart=HH:MM`、`quietEnd=HH:MM`。
|
||||
|
||||
```text
|
||||
/jsc-mod-companion:setup nickname=阿明 wake=07:30 sleep=23:30 tz=+0800 careLimit=3 quietStart=22:00 quietEnd=08:00
|
||||
```
|
||||
|
||||
### `/jsc-mod-companion:todo [list]`
|
||||
|
||||
列出未完成的待辦。
|
||||
|
||||
```text
|
||||
/jsc-mod-companion:todo
|
||||
```
|
||||
|
||||
### `/jsc-mod-companion:todo add <內容> [due=YYYY-MM-DDTHH:MM]`
|
||||
|
||||
新增待辦,可指定到期時間(依 `tz` 設定換算)。
|
||||
|
||||
```text
|
||||
/jsc-mod-companion:todo add 買牛奶 due=2026-10-15T18:00
|
||||
```
|
||||
|
||||
### `/jsc-mod-companion:todo promise <內容> [due=YYYY-MM-DDTHH:MM]`
|
||||
|
||||
記錄角色答應使用者的事(列表中標示【承諾】),用法同 `todo add`。
|
||||
|
||||
```text
|
||||
/jsc-mod-companion:todo promise 週末一起討論新章節 due=2026-10-17T10:00
|
||||
```
|
||||
|
||||
### `/jsc-mod-companion:todo done <編號>`
|
||||
|
||||
將待辦標為完成。
|
||||
|
||||
```text
|
||||
/jsc-mod-companion:todo done 1
|
||||
```
|
||||
|
||||
### `/jsc-mod-companion:quiet [分鐘]`
|
||||
|
||||
暫時靜音,不主動關心;預設 60 分鐘,`0` 取消靜音。
|
||||
|
||||
```text
|
||||
/jsc-mod-companion:quiet 30
|
||||
```
|
||||
|
||||
### `/jsc-mod-companion:ingest`
|
||||
|
||||
解析單章小說(txt/md),逐一詢問合併、新建或略過後寫入角色資料;提供章節檔或要求建立角色時也會自動使用。
|
||||
|
||||
```text
|
||||
/jsc-mod-companion:ingest chapter1.md 章節編號 1
|
||||
```
|
||||
+1
-1
@@ -140,7 +140,7 @@ describe('關心:靜音與不打斷', () => {
|
||||
assert.deepEqual((await s.care.tick()).map(c => c.trigger), ['session_start'])
|
||||
})
|
||||
|
||||
test('/companion quiet 在期間內靜音,到期後恢復', async () => {
|
||||
test('/jsc-mod-companion:quiet 在期間內靜音,到期後恢復', async () => {
|
||||
const s = setup()
|
||||
await s.care.quiet(30)
|
||||
await s.care.onUserTurn()
|
||||
+1
-1
@@ -393,7 +393,7 @@ export interface CareEngine {
|
||||
onTurnComplete(usedTools: boolean): Promise<void>
|
||||
/** 定時/事件檢查:回傳要立即顯示的 toast 或氣泡 */
|
||||
tick(signals?: Signals): Promise<Care[]>
|
||||
/** /companion quiet [分鐘],0 取消靜音 */
|
||||
/** /jsc-mod-companion:quiet [分鐘],0 取消靜音 */
|
||||
quiet(minutes: number): Promise<void>
|
||||
state(): Promise<CareState>
|
||||
}
|
||||
+6
-6
@@ -152,7 +152,7 @@ export function createCompanion({ fs, clock, root, builtins = [FUBUKI] }: Compan
|
||||
else if (k === 'quietEnd') u.quiet.end = val
|
||||
else (u as Record<string, unknown>)[k] = val
|
||||
}
|
||||
if (bad.length) return `無法解析:${bad.join(' ')}\n用法:/companion setup nickname=… wake=HH:MM sleep=HH:MM tz=+0800 careLimit=N quietStart=HH:MM quietEnd=HH:MM`
|
||||
if (bad.length) return `無法解析:${bad.join(' ')}\n用法:/jsc-mod-companion:setup nickname=… wake=HH:MM sleep=HH:MM tz=+0800 careLimit=N quietStart=HH:MM quietEnd=HH:MM`
|
||||
if (args.length) await writeJson(fs, userPath, u)
|
||||
return [
|
||||
args.length ? '設定已儲存:' : '目前設定(以 key=value 修改):',
|
||||
@@ -165,7 +165,7 @@ export function createCompanion({ fs, clock, root, builtins = [FUBUKI] }: Compan
|
||||
async function command(raw: string): Promise<string> {
|
||||
const [sub = '', ...rest] = raw.trim().split(/\s+/).filter(Boolean)
|
||||
if (sub === 'status') return status()
|
||||
if (sub === 'switch') return rest[0] ? switchTo(rest[0]) : `用法:/companion switch <角色>。可用:${(await available()).join('、')}`
|
||||
if (sub === 'switch') return rest[0] ? switchTo(rest[0]) : `用法:/jsc-mod-companion:switch <角色>。可用:${(await available()).join('、')}`
|
||||
if (sub === 'off') {
|
||||
const g = await getGlobal()
|
||||
await writeJson(fs, globalPath, { ...g, active: null, activatedOnce: true })
|
||||
@@ -174,7 +174,7 @@ export function createCompanion({ fs, clock, root, builtins = [FUBUKI] }: Compan
|
||||
if (sub === 'setup') return setup(rest)
|
||||
if (sub === 'todo') return todoCommand(rest)
|
||||
if (sub === 'quiet') return quietCommand(rest)
|
||||
return '用法:/companion status | switch <角色> | off | setup [key=value…] | todo … | quiet [分鐘]'
|
||||
return '用法:/jsc-mod-companion:status | :switch <角色> | :disable | :setup [key=value…] | :todo … | :quiet [分鐘]'
|
||||
}
|
||||
|
||||
const CARE_STAGES: CareProfile['stage'][] = ['stranger', 'familiar', 'close', 'special']
|
||||
@@ -300,21 +300,21 @@ export function createCompanion({ fs, clock, root, builtins = [FUBUKI] }: Compan
|
||||
return false
|
||||
})
|
||||
const title = words.join(' ').trim()
|
||||
if (!title) return '用法:/companion todo add <內容> [due=YYYY-MM-DDTHH:MM]'
|
||||
if (!title) return '用法:/jsc-mod-companion:todo add <內容> [due=YYYY-MM-DDTHH:MM]'
|
||||
return `已加入:${fmtTodo(await b.todos.add(title, { promise: sub === 'promise', dueAt }))}`
|
||||
}
|
||||
if (sub === 'done') {
|
||||
const t = await b.todos.complete(Number(args[0]))
|
||||
return t ? `已完成:${fmtTodo(t)}` : `找不到待辦 #${args[0] ?? ''}`
|
||||
}
|
||||
return '用法:/companion todo [list] | add <內容> [due=YYYY-MM-DDTHH:MM] | promise <內容> | done <編號>'
|
||||
return '用法:/jsc-mod-companion:todo [list] | add <內容> [due=YYYY-MM-DDTHH:MM] | promise <內容> | done <編號>'
|
||||
}
|
||||
|
||||
async function quietCommand(rest: string[]): Promise<string> {
|
||||
const b = await bundleFor()
|
||||
if (!b) return '目前沒有啟用角色。'
|
||||
const n = rest[0] === undefined ? 60 : Number(rest[0])
|
||||
if (!Number.isFinite(n) || n < 0) return '用法:/companion quiet [分鐘](0 取消靜音)'
|
||||
if (!Number.isFinite(n) || n < 0) return '用法:/jsc-mod-companion:quiet [分鐘](0 取消靜音)'
|
||||
await b.care.quiet(n)
|
||||
return n > 0 ? `已靜音 ${n} 分鐘。` : '已取消靜音。'
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// 角色行用的字串片段與寬度計算(與 jsc-mod 外掛各自獨立,不互相依賴)
|
||||
// 角色行用的字串片段與寬度計算(與 jsc-mod-statusline 外掛各自獨立,不互相依賴)
|
||||
export type Seg = { text: string; color?: string; dim?: boolean; bold?: boolean }
|
||||
|
||||
export function bar(p: number, cells = 5): string {
|
||||
+15
-9
@@ -13,17 +13,22 @@ 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) => {
|
||||
await $.command.register({
|
||||
name: 'companion',
|
||||
description: '角色陪伴控制:狀態、切換角色、關閉、使用者設定',
|
||||
argumentHint: 'status | switch <角色> | off | setup',
|
||||
})
|
||||
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}。用 /companion off 可關閉`)
|
||||
if (first.first) $.ui.toast(`已啟用 ${first.name}。用 /jsc-mod-companion:disable 可關閉`)
|
||||
|
||||
// 定時檢查 toast 類關心(到期待辦、作息);失敗不影響主流程
|
||||
timer?.cancel()
|
||||
@@ -37,9 +42,10 @@ export const register: Register = on => {
|
||||
return next(e)
|
||||
})
|
||||
|
||||
on('command.run', { command: 'companion' }, async ($, e) => ({
|
||||
text: companion ? await companion.command(e.args) : '尚未就緒。',
|
||||
}))
|
||||
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) => {
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "jsc-mod",
|
||||
"name": "jsc-mod-statusline",
|
||||
"version": "0.0.1",
|
||||
"description": "輸入框上方的單行狀態列:模型、目錄、分支、5h/7d 額度與重置、context、CPU/記憶體/磁碟",
|
||||
"license": "MIT",
|
||||
@@ -0,0 +1,27 @@
|
||||
# jsc-mod-statusline
|
||||
|
||||
## 指令
|
||||
|
||||
### `/jsc-mod-statusline:enable`
|
||||
|
||||
開啟狀態列。
|
||||
|
||||
```text
|
||||
/jsc-mod-statusline:enable
|
||||
```
|
||||
|
||||
### `/jsc-mod-statusline:compact`
|
||||
|
||||
改用極窄版狀態列。
|
||||
|
||||
```text
|
||||
/jsc-mod-statusline:compact
|
||||
```
|
||||
|
||||
### `/jsc-mod-statusline:disable`
|
||||
|
||||
隱藏狀態列。
|
||||
|
||||
```text
|
||||
/jsc-mod-statusline:disable
|
||||
```
|
||||
@@ -91,9 +91,13 @@ describe('format', () => {
|
||||
})
|
||||
|
||||
describe('band', () => {
|
||||
test('draws one line above the prompt and hides with /statusline off', async ($, on) => {
|
||||
test('draws one line above the prompt and hides with /jsc-mod-statusline:disable', async ($, on) => {
|
||||
on('session.model', () => ({ value: 'claude-opus-5-5' }))
|
||||
on('session.cwd', () => ({ value: '/home/dev/work/luopan' }))
|
||||
on('ui.render', ($, e) => {
|
||||
const { Box } = $.ui.resolve(e)
|
||||
return <Box />
|
||||
})
|
||||
const props = {
|
||||
hasSurvey: false,
|
||||
isWorking: false,
|
||||
@@ -103,12 +107,12 @@ describe('band', () => {
|
||||
view: {},
|
||||
}
|
||||
for (const surface of ['terminal', 'desktop'] as const) {
|
||||
const ui = await $.ui.mount({ plugin: 'jsc-mod', surface, component: 'AbovePrompt', props })
|
||||
const ui = await $.ui.mount({ plugin: 'jsc-mod-statusline', surface, component: 'AbovePrompt', props })
|
||||
expect(await ui.find({ type: 'Text', text: /◆ Opus 5\.5/ })).toBeDefined()
|
||||
expect(await ui.find({ type: 'Text', text: /5h —/ })).toBeDefined()
|
||||
await ui.unmount()
|
||||
}
|
||||
const ran = await $.command.run({ command: 'statusline', args: 'off' } as Parameters<typeof $.command.run>[0])
|
||||
const ran = await $.command.run({ command: 'disable', args: '' } as Parameters<typeof $.command.run>[0])
|
||||
expect(JSON.stringify(ran)).toContain('隱藏')
|
||||
})
|
||||
})
|
||||
+13
-15
@@ -5,12 +5,12 @@ import type { Mode, Sys, Usage, Window } from '../types'
|
||||
import { fit, cpuPercent, parseCpu, parseDf, parseGit, parseMem, parseOffset } from './format'
|
||||
import type { CpuSample } from './format'
|
||||
|
||||
const usageAtom = atom({ plugin: 'jsc-mod', key: 'usage' } as const, {})
|
||||
const gitAtom = atom({ plugin: 'jsc-mod', key: 'git' } as const, null)
|
||||
const sysAtom = atom({ plugin: 'jsc-mod', key: 'sys' } as const, {})
|
||||
const modeAtom = atom({ plugin: 'jsc-mod', key: 'mode' } as const, 'auto')
|
||||
const alertedAtom = atom({ plugin: 'jsc-mod', key: 'alerted' } as const, [])
|
||||
const effortAtom = atom({ plugin: 'jsc-mod', key: 'effort' } as const, null)
|
||||
const usageAtom = atom({ plugin: 'jsc-mod-statusline', key: 'usage' } as const, {})
|
||||
const gitAtom = atom({ plugin: 'jsc-mod-statusline', key: 'git' } as const, null)
|
||||
const sysAtom = atom({ plugin: 'jsc-mod-statusline', key: 'sys' } as const, {})
|
||||
const modeAtom = atom({ plugin: 'jsc-mod-statusline', key: 'mode' } as const, 'auto')
|
||||
const alertedAtom = atom({ plugin: 'jsc-mod-statusline', key: 'alerted' } as const, [])
|
||||
const effortAtom = atom({ plugin: 'jsc-mod-statusline', key: 'effort' } as const, null)
|
||||
|
||||
const SYS_MS = 3000
|
||||
const GIT_MS = 15_000
|
||||
@@ -99,9 +99,9 @@ async function alert($: EngineInterface, rateLimits: SessionRateLimit[]) {
|
||||
}
|
||||
}
|
||||
|
||||
async function runHud($: EngineInterface, args: string) {
|
||||
const arg = args.trim()
|
||||
const mode: Mode = arg === 'off' ? 'off' : arg === 'compact' ? 'compact' : 'auto'
|
||||
const MODES: Record<string, Mode> = { 'enable': 'auto', 'compact': 'compact', 'disable': 'off' }
|
||||
|
||||
async function runHud($: EngineInterface, mode: Mode) {
|
||||
await update($, modeAtom, () => mode)
|
||||
const said: Record<Mode, string> = { auto: '狀態列已開啟。', off: '狀態列已隱藏。', compact: '狀態列改用極窄版。' }
|
||||
|
||||
@@ -110,11 +110,9 @@ async function runHud($: EngineInterface, args: string) {
|
||||
|
||||
export const register: Register = on => {
|
||||
on('session.start', async ($, e, next) => {
|
||||
await $.command.register({
|
||||
name: 'statusline',
|
||||
description: '切換輸入框上方的狀態列',
|
||||
argumentHint: 'on | off | compact',
|
||||
})
|
||||
await $.command.register({ name: 'enable', description: '開啟輸入框上方的狀態列' })
|
||||
await $.command.register({ name: 'compact', description: '輸入框上方的狀態列改用極窄版' })
|
||||
await $.command.register({ name: 'disable', description: '隱藏輸入框上方的狀態列' })
|
||||
home = await $.env.get('HOME')
|
||||
try {
|
||||
const r = await $.process.run(['date', '+%z'], { timeoutMs: 3000 })
|
||||
@@ -161,7 +159,7 @@ export const register: Register = on => {
|
||||
return ran
|
||||
})
|
||||
|
||||
on('command.run', { command: 'statusline' }, ($, e) => runHud($, e.args))
|
||||
for (const [command, mode] of Object.entries(MODES)) on('command.run', { command }, $ => runHud($, mode))
|
||||
|
||||
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
|
||||
const mode = await read($, modeAtom)
|
||||
+1
-1
@@ -16,7 +16,7 @@ export type Mode = 'auto' | 'compact' | 'off'
|
||||
|
||||
declare module 'claude-code' {
|
||||
interface PluginState {
|
||||
'jsc-mod': {
|
||||
'jsc-mod-statusline': {
|
||||
usage: Usage
|
||||
git: Git | null
|
||||
sys: Sys
|
||||
@@ -1,27 +0,0 @@
|
||||
# jsc-mod
|
||||
|
||||
## 指令
|
||||
|
||||
### `/jsc-mod:statusline on`
|
||||
|
||||
開啟狀態列。
|
||||
|
||||
```text
|
||||
/jsc-mod:statusline on
|
||||
```
|
||||
|
||||
### `/jsc-mod:statusline off`
|
||||
|
||||
隱藏狀態列。
|
||||
|
||||
```text
|
||||
/jsc-mod:statusline off
|
||||
```
|
||||
|
||||
### `/jsc-mod:statusline compact`
|
||||
|
||||
改用極窄版狀態列。
|
||||
|
||||
```text
|
||||
/jsc-mod:statusline compact
|
||||
```
|
||||
Reference in New Issue
Block a user