dsh-working-activity 0.2.6 → 0.3.2
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/LICENSE +28 -28
- package/README.i18n.yaml +6 -6
- package/README.md +122 -122
- package/README.zh.md +113 -113
- package/cordis.patch.yml +19 -19
- package/lib/client.js +6 -6
- package/lib/client.js.map +1 -1
- package/lib/types/config.d.ts +68 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/config.js +141 -0
- package/lib/types/frames.d.ts +37 -0
- package/lib/types/frames.d.ts.map +1 -0
- package/lib/types/frames.js +126 -0
- package/lib/types/index.d.ts +16 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +35 -6
- package/lib/types/lang.d.ts +100 -0
- package/lib/types/lang.d.ts.map +1 -0
- package/lib/types/lang.js +132 -0
- package/lib/types/phrases.d.ts +125 -6
- package/lib/types/phrases.d.ts.map +1 -1
- package/lib/types/phrases.js +362 -27
- package/lib/types/status.d.ts +62 -0
- package/lib/types/status.d.ts.map +1 -1
- package/lib/types/status.js +202 -22
- package/package.json +12 -2
- package/src/client/WorkingLine.module.css +74 -74
- package/src/client/WorkingLine.tsx +42 -42
- package/src/client/activity.ts +50 -50
- package/src/client/css-modules.d.ts +6 -6
- package/src/client/index.ts +42 -42
- package/src/config.ts +174 -0
- package/src/events.ts +45 -45
- package/src/frames.ts +138 -0
- package/src/index.ts +278 -228
- package/src/invariant.ts +69 -69
- package/src/lang.ts +145 -0
- package/src/phrases.ts +584 -180
- package/src/registration.ts +99 -99
- package/src/status.ts +706 -501
package/src/client/activity.ts
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Client-side activity snapshot type + the ConversationSnapshot merge that
|
|
3
|
-
* carries it.
|
|
4
|
-
*
|
|
5
|
-
* The host half (@deepseek-ai/dsh-working-activity's node side) publishes
|
|
6
|
-
* `activity/status` session events; the web runtime patch (dsh-client-runtime
|
|
7
|
-
* activity field, shipping separately) narrows those frames into the
|
|
8
|
-
* conversation snapshot's `activity` member. Until that runtime release lands
|
|
9
|
-
* on npm, this module re-declares the field via declaration merging so the
|
|
10
|
-
* dock component type-checks against the published rc.6 types.
|
|
11
|
-
*
|
|
12
|
-
* REMOVE the `declare module` block below once
|
|
13
|
-
* `@deepseek-ai/dsh-client-runtime/client` ships `ConversationSnapshot.activity`
|
|
14
|
-
* natively (the runtime's own `ActivityStatusView` is structurally identical
|
|
15
|
-
* to {@link ActivitySnapshot}; the two then agree by construction).
|
|
16
|
-
* @module @deepseek-ai/dsh-working-activity/client/activity
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
/** The `activity/status` phase vocabulary, mirroring the host's ActivityPhase. */
|
|
20
|
-
export type ActivityPhase = 'idle' | 'waiting' | 'thinking' | 'tool' | 'done'
|
|
21
|
-
|
|
22
|
-
/** One `activity/status` snapshot as rendered by the dock line. */
|
|
23
|
-
export interface ActivitySnapshot {
|
|
24
|
-
/** Which activity phase the model is in right now. */
|
|
25
|
-
readonly phase: ActivityPhase
|
|
26
|
-
/** Full human-readable status line (plain text, no ANSI). */
|
|
27
|
-
readonly line: string
|
|
28
|
-
/** Short label of the current work (tool action or stage), when any. */
|
|
29
|
-
readonly label?: string
|
|
30
|
-
/** Detail fragment (path / command / search pattern), when any. */
|
|
31
|
-
readonly detail?: string
|
|
32
|
-
/** The playful phrase currently shown, when any. */
|
|
33
|
-
readonly phrase?: string
|
|
34
|
-
/** Tools completed in the current turn. */
|
|
35
|
-
readonly toolCount: number
|
|
36
|
-
/** Wall-clock milliseconds since the current turn started (0 when idle). */
|
|
37
|
-
readonly turnElapsedMs: number
|
|
38
|
-
/** Wall-clock time the current phase started, for animations. */
|
|
39
|
-
readonly phaseStartedAt: number
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
declare module '@deepseek-ai/dsh-client-runtime/client' {
|
|
43
|
-
interface ConversationSnapshot {
|
|
44
|
-
/**
|
|
45
|
-
* Latest validated `activity/status` snapshot (the working-activity
|
|
46
|
-
* plugin's live working line), or null before the first publish.
|
|
47
|
-
*/
|
|
48
|
-
activity: ActivitySnapshot | null
|
|
49
|
-
}
|
|
50
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Client-side activity snapshot type + the ConversationSnapshot merge that
|
|
3
|
+
* carries it.
|
|
4
|
+
*
|
|
5
|
+
* The host half (@deepseek-ai/dsh-working-activity's node side) publishes
|
|
6
|
+
* `activity/status` session events; the web runtime patch (dsh-client-runtime
|
|
7
|
+
* activity field, shipping separately) narrows those frames into the
|
|
8
|
+
* conversation snapshot's `activity` member. Until that runtime release lands
|
|
9
|
+
* on npm, this module re-declares the field via declaration merging so the
|
|
10
|
+
* dock component type-checks against the published rc.6 types.
|
|
11
|
+
*
|
|
12
|
+
* REMOVE the `declare module` block below once
|
|
13
|
+
* `@deepseek-ai/dsh-client-runtime/client` ships `ConversationSnapshot.activity`
|
|
14
|
+
* natively (the runtime's own `ActivityStatusView` is structurally identical
|
|
15
|
+
* to {@link ActivitySnapshot}; the two then agree by construction).
|
|
16
|
+
* @module @deepseek-ai/dsh-working-activity/client/activity
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** The `activity/status` phase vocabulary, mirroring the host's ActivityPhase. */
|
|
20
|
+
export type ActivityPhase = 'idle' | 'waiting' | 'thinking' | 'tool' | 'done'
|
|
21
|
+
|
|
22
|
+
/** One `activity/status` snapshot as rendered by the dock line. */
|
|
23
|
+
export interface ActivitySnapshot {
|
|
24
|
+
/** Which activity phase the model is in right now. */
|
|
25
|
+
readonly phase: ActivityPhase
|
|
26
|
+
/** Full human-readable status line (plain text, no ANSI). */
|
|
27
|
+
readonly line: string
|
|
28
|
+
/** Short label of the current work (tool action or stage), when any. */
|
|
29
|
+
readonly label?: string
|
|
30
|
+
/** Detail fragment (path / command / search pattern), when any. */
|
|
31
|
+
readonly detail?: string
|
|
32
|
+
/** The playful phrase currently shown, when any. */
|
|
33
|
+
readonly phrase?: string
|
|
34
|
+
/** Tools completed in the current turn. */
|
|
35
|
+
readonly toolCount: number
|
|
36
|
+
/** Wall-clock milliseconds since the current turn started (0 when idle). */
|
|
37
|
+
readonly turnElapsedMs: number
|
|
38
|
+
/** Wall-clock time the current phase started, for animations. */
|
|
39
|
+
readonly phaseStartedAt: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare module '@deepseek-ai/dsh-client-runtime/client' {
|
|
43
|
+
interface ConversationSnapshot {
|
|
44
|
+
/**
|
|
45
|
+
* Latest validated `activity/status` snapshot (the working-activity
|
|
46
|
+
* plugin's live working line), or null before the first publish.
|
|
47
|
+
*/
|
|
48
|
+
activity: ActivitySnapshot | null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/* CSS Modules ambient declaration for the client half (tsc only; the tsdown
|
|
2
|
-
bundle inlines compiled class maps). */
|
|
3
|
-
declare module '*.module.css' {
|
|
4
|
-
const classes: Readonly<Record<string, string>>
|
|
5
|
-
export default classes
|
|
6
|
-
}
|
|
1
|
+
/* CSS Modules ambient declaration for the client half (tsc only; the tsdown
|
|
2
|
+
bundle inlines compiled class maps). */
|
|
3
|
+
declare module '*.module.css' {
|
|
4
|
+
const classes: Readonly<Record<string, string>>
|
|
5
|
+
export default classes
|
|
6
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Working-activity surface plugin, browser half: the working-line entry in
|
|
3
|
-
* the conversation.input.dock strip. This package's node half emits
|
|
4
|
-
* `activity/status` session events; the web runtime patch narrows them into
|
|
5
|
-
* the conversation snapshot's `activity` member, so this dock entry reads
|
|
6
|
-
* the live frame through the standard session kit and owns no store, no
|
|
7
|
-
* refresh chain, and no event listener.
|
|
8
|
-
*
|
|
9
|
-
* Mount contract (see the root README's "Web UI 集成" section): the web
|
|
10
|
-
* client's client-modules host scans loader entries for `dsh.client`
|
|
11
|
-
* declarations and serves this package's `./client` bundle at
|
|
12
|
-
* /plugins/dsh-working-activity/client.js. The entry contributes into the
|
|
13
|
-
* input dock — no official-source patch is involved.
|
|
14
|
-
*/
|
|
15
|
-
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
16
|
-
// Type-only: pulls the ui-conversation SlotMap merge (the input.dock entry)
|
|
17
|
-
// through the Client assembly boundary.
|
|
18
|
-
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
19
|
-
import { WorkingLine } from './WorkingLine.tsx'
|
|
20
|
-
import type { ActivityPhase, ActivitySnapshot } from './activity.ts'
|
|
21
|
-
|
|
22
|
-
export { WorkingLine, type WorkingLineProps } from './WorkingLine.tsx'
|
|
23
|
-
export type { ActivityPhase, ActivitySnapshot } from './activity.ts'
|
|
24
|
-
|
|
25
|
-
/** Required services for the dock registration. */
|
|
26
|
-
export const inject = ['slots']
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Client plugin body: the working-line dock entry.
|
|
30
|
-
* @param ctx - client root context.
|
|
31
|
-
*/
|
|
32
|
-
export function apply(ctx: ClientContext): void {
|
|
33
|
-
ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
|
|
34
|
-
name: 'conversation.input.dock',
|
|
35
|
-
// The pre-slots patch used the same id in ui-conversation; entries with
|
|
36
|
-
// equal order render in registration order, and goal (10) / queue (20)
|
|
37
|
-
// keep their seats — this row sits between them.
|
|
38
|
-
id: 'activity',
|
|
39
|
-
order: 15,
|
|
40
|
-
registrant: 'dsh-working-activity',
|
|
41
|
-
}, WorkingLine))
|
|
42
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Working-activity surface plugin, browser half: the working-line entry in
|
|
3
|
+
* the conversation.input.dock strip. This package's node half emits
|
|
4
|
+
* `activity/status` session events; the web runtime patch narrows them into
|
|
5
|
+
* the conversation snapshot's `activity` member, so this dock entry reads
|
|
6
|
+
* the live frame through the standard session kit and owns no store, no
|
|
7
|
+
* refresh chain, and no event listener.
|
|
8
|
+
*
|
|
9
|
+
* Mount contract (see the root README's "Web UI 集成" section): the web
|
|
10
|
+
* client's client-modules host scans loader entries for `dsh.client`
|
|
11
|
+
* declarations and serves this package's `./client` bundle at
|
|
12
|
+
* /plugins/dsh-working-activity/client.js. The entry contributes into the
|
|
13
|
+
* input dock — no official-source patch is involved.
|
|
14
|
+
*/
|
|
15
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
16
|
+
// Type-only: pulls the ui-conversation SlotMap merge (the input.dock entry)
|
|
17
|
+
// through the Client assembly boundary.
|
|
18
|
+
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
19
|
+
import { WorkingLine } from './WorkingLine.tsx'
|
|
20
|
+
import type { ActivityPhase, ActivitySnapshot } from './activity.ts'
|
|
21
|
+
|
|
22
|
+
export { WorkingLine, type WorkingLineProps } from './WorkingLine.tsx'
|
|
23
|
+
export type { ActivityPhase, ActivitySnapshot } from './activity.ts'
|
|
24
|
+
|
|
25
|
+
/** Required services for the dock registration. */
|
|
26
|
+
export const inject = ['slots']
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Client plugin body: the working-line dock entry.
|
|
30
|
+
* @param ctx - client root context.
|
|
31
|
+
*/
|
|
32
|
+
export function apply(ctx: ClientContext): void {
|
|
33
|
+
ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
|
|
34
|
+
name: 'conversation.input.dock',
|
|
35
|
+
// The pre-slots patch used the same id in ui-conversation; entries with
|
|
36
|
+
// equal order render in registration order, and goal (10) / queue (20)
|
|
37
|
+
// keep their seats — this row sits between them.
|
|
38
|
+
id: 'activity',
|
|
39
|
+
order: 15,
|
|
40
|
+
registrant: 'dsh-working-activity',
|
|
41
|
+
}, WorkingLine))
|
|
42
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Working-activity user configuration, mirroring the pi extension's
|
|
3
|
+
* `~/.pi/agent/working-activity.json` shape: the UI owns the file (dsh-tui
|
|
4
|
+
* persists `frames` at `~/.dsh-tui/working-activity.json`) and consumers use
|
|
5
|
+
* the pure parser here to honor `mode` / `features` / `customPhrases` /
|
|
6
|
+
* `customActions` without duplicating validation. No I/O — filesystem
|
|
7
|
+
* ownership stays with the host.
|
|
8
|
+
* @module @deepseek-ai/dsh-working-activity/config
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { DEFAULT_PRESET, isPresetName } from './frames.js'
|
|
12
|
+
|
|
13
|
+
/** Feature flags that can be switched independently (pi extension parity). */
|
|
14
|
+
export const FEATURE_FLAGS = [
|
|
15
|
+
'phrases', // 俏皮文案池(思考/等待/收尾/工具动词)
|
|
16
|
+
'rareEggs', // 稀有彩虹彩蛋
|
|
17
|
+
'nightPhrases', // 深夜文案
|
|
18
|
+
'weekend', // 周末问候
|
|
19
|
+
'holidays', // 节假日彩蛋
|
|
20
|
+
'combo', // 连击火力全开
|
|
21
|
+
'failPhrases', // 失败文案池
|
|
22
|
+
'modelQuips', // 模型切换梗
|
|
23
|
+
'shimmer', // 文案星辉扫过/彩虹流光
|
|
24
|
+
'continuePhrases', // 打断后接梗
|
|
25
|
+
'cost', // 成本与 token 核算(结束时展示)
|
|
26
|
+
] as const
|
|
27
|
+
|
|
28
|
+
/** Feature-flag name type. */
|
|
29
|
+
export type FeatureFlag = (typeof FEATURE_FLAGS)[number]
|
|
30
|
+
|
|
31
|
+
/** Chinese labels for the settings panel / docs. */
|
|
32
|
+
export const FEATURE_LABELS: Record<FeatureFlag, string> = {
|
|
33
|
+
phrases: '俏皮文案',
|
|
34
|
+
rareEggs: '稀有彩蛋',
|
|
35
|
+
nightPhrases: '深夜文案',
|
|
36
|
+
weekend: '周末问候',
|
|
37
|
+
holidays: '节假日彩蛋',
|
|
38
|
+
combo: '工具连击',
|
|
39
|
+
failPhrases: '失败文案',
|
|
40
|
+
modelQuips: '模型切换梗',
|
|
41
|
+
shimmer: '星辉效果',
|
|
42
|
+
continuePhrases: '打断接梗',
|
|
43
|
+
cost: '成本与 token',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The file-based config, same keys as the pi extension. */
|
|
47
|
+
export type WorkingActivityConfig = {
|
|
48
|
+
/** Frame preset name (`random` picks one per process). */
|
|
49
|
+
frames?: string
|
|
50
|
+
/** Extra thinking phrases appended to the base pool. */
|
|
51
|
+
customPhrases?: readonly string[]
|
|
52
|
+
/** Exact tool-name → action-copy pools (case-insensitive match). */
|
|
53
|
+
customActions?: Readonly<Record<string, readonly string[]>>
|
|
54
|
+
/** Inject the `⏵` self-narration contract into the system prompt. */
|
|
55
|
+
narrate?: boolean
|
|
56
|
+
/** Debug logging to `working-activity-debug.log`. */
|
|
57
|
+
debugLog?: boolean
|
|
58
|
+
/** Context usage warning threshold in percent. */
|
|
59
|
+
contextWarnAt?: number
|
|
60
|
+
/** Context usage danger threshold in percent. */
|
|
61
|
+
contextDangerAt?: number
|
|
62
|
+
/** Show estimated output tokens per second. */
|
|
63
|
+
showTokPerSec?: boolean
|
|
64
|
+
/** Hourly work-reminder threshold (0–24, 0 = off). */
|
|
65
|
+
workRemindAt?: number
|
|
66
|
+
/** lively: full flourish (default) / minimal: functional labels only. */
|
|
67
|
+
mode?: 'lively' | 'minimal'
|
|
68
|
+
/** Per-feature switches; explicit values override `mode` defaults. */
|
|
69
|
+
features?: Readonly<Record<string, boolean>>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The defaults when nothing is configured. */
|
|
73
|
+
export const DEFAULT_ACTIVITY_CONFIG: WorkingActivityConfig = {
|
|
74
|
+
frames: DEFAULT_PRESET,
|
|
75
|
+
narrate: true,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Result of parsing a config file. */
|
|
79
|
+
export type ConfigReadResult = {
|
|
80
|
+
readonly config: WorkingActivityConfig
|
|
81
|
+
/** Raw parsed object, preserving unknown keys for lossless write-back. */
|
|
82
|
+
readonly raw: Record<string, unknown>
|
|
83
|
+
/** Parse error message, when the text was not a valid config object. */
|
|
84
|
+
readonly error?: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function errorText(error: unknown): string {
|
|
88
|
+
return error instanceof Error ? error.message : String(error)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Clamp `dangerAt` to be at least `warnAt` (percent thresholds). */
|
|
92
|
+
export function normalizeThresholds(cfg: WorkingActivityConfig): WorkingActivityConfig {
|
|
93
|
+
const warnAt = cfg.contextWarnAt ?? 80
|
|
94
|
+
const dangerAt = cfg.contextDangerAt ?? 95
|
|
95
|
+
return dangerAt < warnAt ? { ...cfg, contextDangerAt: warnAt } : cfg
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Parse a `working-activity.json` text into a validated config. Unknown keys
|
|
100
|
+
* are preserved in `raw` (for lossless write-back); invalid values are
|
|
101
|
+
* dropped. A non-object root yields the defaults plus an `error`.
|
|
102
|
+
* @param text - Raw file contents.
|
|
103
|
+
* @returns Validated config + raw object (+ optional error).
|
|
104
|
+
*/
|
|
105
|
+
export function parseWorkingActivityConfig(text: string): ConfigReadResult {
|
|
106
|
+
let parsed: unknown
|
|
107
|
+
try {
|
|
108
|
+
parsed = JSON.parse(text)
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return { config: { ...DEFAULT_ACTIVITY_CONFIG }, raw: {}, error: errorText(error) }
|
|
111
|
+
}
|
|
112
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
113
|
+
return {
|
|
114
|
+
config: { ...DEFAULT_ACTIVITY_CONFIG },
|
|
115
|
+
raw: {},
|
|
116
|
+
error: '配置根节点必须是 JSON 对象',
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const raw = parsed as Record<string, unknown>
|
|
120
|
+
const config: WorkingActivityConfig = { ...DEFAULT_ACTIVITY_CONFIG }
|
|
121
|
+
if (typeof raw.frames === 'string' && isPresetName(raw.frames)) config.frames = raw.frames
|
|
122
|
+
if (Array.isArray(raw.customPhrases)) {
|
|
123
|
+
const phrases = raw.customPhrases.filter((s): s is string => typeof s === 'string' && s.trim().length > 0)
|
|
124
|
+
if (phrases.length > 0) config.customPhrases = phrases
|
|
125
|
+
}
|
|
126
|
+
if (raw.customActions !== null && typeof raw.customActions === 'object' && !Array.isArray(raw.customActions)) {
|
|
127
|
+
const customActions: Record<string, string[]> = {}
|
|
128
|
+
for (const [key, value] of Object.entries(raw.customActions)) {
|
|
129
|
+
const actions = Array.isArray(value)
|
|
130
|
+
? value.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
|
|
131
|
+
.map((entry) => entry.trim())
|
|
132
|
+
: []
|
|
133
|
+
if (key.trim().length > 0 && actions.length > 0) customActions[key.trim()] = actions
|
|
134
|
+
}
|
|
135
|
+
if (Object.keys(customActions).length > 0) config.customActions = customActions
|
|
136
|
+
}
|
|
137
|
+
if (typeof raw.narrate === 'boolean') config.narrate = raw.narrate
|
|
138
|
+
if (typeof raw.debugLog === 'boolean') config.debugLog = raw.debugLog
|
|
139
|
+
if (typeof raw.contextWarnAt === 'number' && Number.isFinite(raw.contextWarnAt)
|
|
140
|
+
&& raw.contextWarnAt >= 0 && raw.contextWarnAt <= 100) {
|
|
141
|
+
config.contextWarnAt = raw.contextWarnAt
|
|
142
|
+
}
|
|
143
|
+
if (typeof raw.contextDangerAt === 'number' && Number.isFinite(raw.contextDangerAt)
|
|
144
|
+
&& raw.contextDangerAt >= 0 && raw.contextDangerAt <= 100) {
|
|
145
|
+
config.contextDangerAt = raw.contextDangerAt
|
|
146
|
+
}
|
|
147
|
+
if (typeof raw.showTokPerSec === 'boolean') config.showTokPerSec = raw.showTokPerSec
|
|
148
|
+
if (typeof raw.workRemindAt === 'number' && Number.isFinite(raw.workRemindAt)
|
|
149
|
+
&& raw.workRemindAt >= 0 && raw.workRemindAt <= 24) {
|
|
150
|
+
config.workRemindAt = raw.workRemindAt
|
|
151
|
+
}
|
|
152
|
+
if (raw.mode === 'lively' || raw.mode === 'minimal') config.mode = raw.mode
|
|
153
|
+
if (raw.features !== null && typeof raw.features === 'object' && !Array.isArray(raw.features)) {
|
|
154
|
+
const features: Record<string, boolean> = {}
|
|
155
|
+
for (const [key, value] of Object.entries(raw.features)) {
|
|
156
|
+
if (typeof value === 'boolean') features[key] = value
|
|
157
|
+
}
|
|
158
|
+
if (Object.keys(features).length > 0) config.features = features
|
|
159
|
+
}
|
|
160
|
+
return { config: normalizeThresholds(config), raw }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Whether a feature is on. Explicit `features` entries win; otherwise
|
|
165
|
+
* `minimal` mode turns everything off (except `cost`, always on).
|
|
166
|
+
* @param config - Parsed config.
|
|
167
|
+
* @param name - Feature flag name.
|
|
168
|
+
*/
|
|
169
|
+
export function featureOn(config: WorkingActivityConfig, name: FeatureFlag): boolean {
|
|
170
|
+
const explicit = config.features?.[name]
|
|
171
|
+
if (typeof explicit === 'boolean') return explicit
|
|
172
|
+
if (name === 'cost') return true
|
|
173
|
+
return config.mode !== 'minimal'
|
|
174
|
+
}
|
package/src/events.ts
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `activity/status` session event — a log-only, non-surface snapshot of the
|
|
3
|
-
* model's current working activity, published by this plugin for any UI
|
|
4
|
-
* consumer (Web client, telemetry, …). It never enters derived model history
|
|
5
|
-
* (no `surfaceOp`), so it cannot leak into prompts; UIs render it like
|
|
6
|
-
* `todo/write` or `plan/mode`.
|
|
7
|
-
* @module @deepseek-ai/dsh-working-activity/events
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { ActivityPhase } from './status.js'
|
|
11
|
-
|
|
12
|
-
/** Durable payload of one `activity/status` snapshot. */
|
|
13
|
-
export interface ActivityStatusEvent {
|
|
14
|
-
/** Which activity phase the model is in. */
|
|
15
|
-
readonly phase: ActivityPhase
|
|
16
|
-
/** Human-readable status line (plain text, no ANSI). */
|
|
17
|
-
readonly line: string
|
|
18
|
-
/** Short label of the current work, when any. */
|
|
19
|
-
readonly label?: string
|
|
20
|
-
/** Detail fragment (path / command / pattern), when any. */
|
|
21
|
-
readonly detail?: string
|
|
22
|
-
/** The playful phrase currently shown, when the copy pool is on. */
|
|
23
|
-
readonly phrase?: string
|
|
24
|
-
/** Tools completed in the current turn. */
|
|
25
|
-
readonly toolCount: number
|
|
26
|
-
/** Milliseconds since the current turn started (0 when idle). */
|
|
27
|
-
readonly turnElapsedMs: number
|
|
28
|
-
/** Wall-clock time (epoch ms) the current phase started, for animations. */
|
|
29
|
-
readonly phaseStartedAt: number
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** The `activity/status` phase vocabulary, exported for wire consumers. */
|
|
33
|
-
export type { ActivityPhase }
|
|
34
|
-
|
|
35
|
-
declare module '@deepseek-ai/dsh-session/types' {
|
|
36
|
-
interface SessionEventMap {
|
|
37
|
-
/**
|
|
38
|
-
* Log-only UI snapshot of the model's working activity (thinking copy,
|
|
39
|
-
* running tool, turn elapsed). Never a surface event: UIs render it, the
|
|
40
|
-
* model never sees it.
|
|
41
|
-
* @param data - The rendered status snapshot.
|
|
42
|
-
*/
|
|
43
|
-
'activity/status': ActivityStatusEvent
|
|
44
|
-
}
|
|
45
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* `activity/status` session event — a log-only, non-surface snapshot of the
|
|
3
|
+
* model's current working activity, published by this plugin for any UI
|
|
4
|
+
* consumer (Web client, telemetry, …). It never enters derived model history
|
|
5
|
+
* (no `surfaceOp`), so it cannot leak into prompts; UIs render it like
|
|
6
|
+
* `todo/write` or `plan/mode`.
|
|
7
|
+
* @module @deepseek-ai/dsh-working-activity/events
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ActivityPhase } from './status.js'
|
|
11
|
+
|
|
12
|
+
/** Durable payload of one `activity/status` snapshot. */
|
|
13
|
+
export interface ActivityStatusEvent {
|
|
14
|
+
/** Which activity phase the model is in. */
|
|
15
|
+
readonly phase: ActivityPhase
|
|
16
|
+
/** Human-readable status line (plain text, no ANSI). */
|
|
17
|
+
readonly line: string
|
|
18
|
+
/** Short label of the current work, when any. */
|
|
19
|
+
readonly label?: string
|
|
20
|
+
/** Detail fragment (path / command / pattern), when any. */
|
|
21
|
+
readonly detail?: string
|
|
22
|
+
/** The playful phrase currently shown, when the copy pool is on. */
|
|
23
|
+
readonly phrase?: string
|
|
24
|
+
/** Tools completed in the current turn. */
|
|
25
|
+
readonly toolCount: number
|
|
26
|
+
/** Milliseconds since the current turn started (0 when idle). */
|
|
27
|
+
readonly turnElapsedMs: number
|
|
28
|
+
/** Wall-clock time (epoch ms) the current phase started, for animations. */
|
|
29
|
+
readonly phaseStartedAt: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The `activity/status` phase vocabulary, exported for wire consumers. */
|
|
33
|
+
export type { ActivityPhase }
|
|
34
|
+
|
|
35
|
+
declare module '@deepseek-ai/dsh-session/types' {
|
|
36
|
+
interface SessionEventMap {
|
|
37
|
+
/**
|
|
38
|
+
* Log-only UI snapshot of the model's working activity (thinking copy,
|
|
39
|
+
* running tool, turn elapsed). Never a surface event: UIs render it, the
|
|
40
|
+
* model never sees it.
|
|
41
|
+
* @param data - The rendered status snapshot.
|
|
42
|
+
*/
|
|
43
|
+
'activity/status': ActivityStatusEvent
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/frames.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Working-activity indicator frame presets — the single home for every
|
|
3
|
+
* spinner animation, ported from the pi working-activity extension
|
|
4
|
+
* (`FRAME_PRESETS`) plus the dsh-tui additions (moon8 / rainbow / whale
|
|
5
|
+
* family / clock / traffic lights). Consumers (TUI row, Web slot) render the
|
|
6
|
+
* current frame next to the live working line; the preset name is chosen by
|
|
7
|
+
* the UI and persisted in `working-activity.json` (`frames` key).
|
|
8
|
+
*
|
|
9
|
+
* `\uFE0E` forces text rendering so Windows never paints the glyphs as color
|
|
10
|
+
* emoji (the green-block problem); emoji presets (moon8, clock, whales)
|
|
11
|
+
* deliberately omit it to keep colorful rendering on modern terminals.
|
|
12
|
+
* @module @deepseek-ai/dsh-working-activity/frames
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Text-variant selector: keep symbols monochrome on Windows. */
|
|
16
|
+
const TE = '\uFE0E'
|
|
17
|
+
|
|
18
|
+
/** One working-activity preset: the frame sequence and the per-frame interval. */
|
|
19
|
+
export interface FramePreset {
|
|
20
|
+
readonly frames: readonly string[]
|
|
21
|
+
readonly intervalMs: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Named working-activity frame presets, keyed by preset name (`claude`, `moon`, `sand`, ...). */
|
|
25
|
+
export const FRAME_PRESETS: Record<string, FramePreset> = {
|
|
26
|
+
// Claude Code's real sequence: · ✢ * ✶ ✻ ✽ forward + backward.
|
|
27
|
+
claude: {
|
|
28
|
+
frames: ['·', `✢${TE}`, '*', `✶${TE}`, `✻${TE}`, `✽${TE}`, `✻${TE}`, `✶${TE}`, '*', `✢${TE}`],
|
|
29
|
+
intervalMs: 150,
|
|
30
|
+
},
|
|
31
|
+
star2: { frames: [`✶${TE}`, `✸${TE}`, `✹${TE}`, `✺${TE}`, `✹${TE}`, `✷${TE}`], intervalMs: 140 },
|
|
32
|
+
sand: {
|
|
33
|
+
frames: ['⠁', '⠂', '⠄', '⡀', '⡈', '⡐', '⡠', '⣀', '⣁', '⣂', '⣄', '⣌', '⣔', '⣤', '⣥', '⣦', '⣮', '⣶', '⣷', '⣿', '⡿', '⠿', '⢟', '⠟', '⡛', '⠛', '⠫', '⢋', '⠋', '⠍', '⡉', '⠉', '⠑', '⠡', '⢁'],
|
|
34
|
+
intervalMs: 120,
|
|
35
|
+
},
|
|
36
|
+
triangle: { frames: ['◢', '◣', '◤', '◥'], intervalMs: 180 },
|
|
37
|
+
box: { frames: ['▖', '▘', '▝', '▗'], intervalMs: 180 },
|
|
38
|
+
box2: { frames: ['▌', '▀', '▐', '▄'], intervalMs: 180 },
|
|
39
|
+
corners: { frames: ['◰', '◳', '◲', '◱'], intervalMs: 190 },
|
|
40
|
+
point: { frames: ['∙∙∙', '●∙∙', '∙●∙', '∙∙●', '∙∙∙'], intervalMs: 190 },
|
|
41
|
+
layer: { frames: ['-', '=', '≡'], intervalMs: 220 },
|
|
42
|
+
flip: { frames: ['_', '_', '_', '-', '`', '`', "'", '´', '-', '_', '_', '_'], intervalMs: 140 },
|
|
43
|
+
aesthetic: {
|
|
44
|
+
frames: ['▰▱▱▱▱▱▱', '▰▰▱▱▱▱▱', '▰▰▰▱▱▱▱', '▰▰▰▰▱▱▱', '▰▰▰▰▰▱▱', '▰▰▰▰▰▰▱', '▰▰▰▰▰▰▰', '▰▱▱▱▱▱▱'],
|
|
45
|
+
intervalMs: 140,
|
|
46
|
+
},
|
|
47
|
+
hamburger: { frames: ['☱', '☲', '☴'], intervalMs: 220 },
|
|
48
|
+
moon: { frames: ['◐', '◓', '◑', '◒'], intervalMs: 240 },
|
|
49
|
+
// kimi-code MoonLoader 同款:8 帧 emoji 月相,120ms 一帧,比半圆版更丝滑。
|
|
50
|
+
// 不带 \uFE0E:保留彩色 emoji 渲染(Windows Terminal 等现代终端效果最佳)。
|
|
51
|
+
moon8: { frames: ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘'], intervalMs: 120 },
|
|
52
|
+
// 鲸鱼喷水:🐳 固定,水柱升起(· → | → ║)再回落,顶珠 ° 模拟水花(对齐 pi 版)。
|
|
53
|
+
'whale-spout': {
|
|
54
|
+
frames: ['🐳 ', '🐳° ', '🐳|°', '🐳║°', '🐳|°', '🐳° ', '🐳 '],
|
|
55
|
+
intervalMs: 160,
|
|
56
|
+
},
|
|
57
|
+
// 鲸鱼转圈:🐳 + 环绕方向指示(逆时针转圈语义,对齐 pi 版)。
|
|
58
|
+
'whale-spin': {
|
|
59
|
+
frames: ['🐳→', '🐳↘', '🐳↓', '🐳↙', '🐳←', '🐳↖', '🐳↑', '🐳↗'],
|
|
60
|
+
intervalMs: 150,
|
|
61
|
+
},
|
|
62
|
+
// 鲸鱼吐泡泡:🐳 固定,泡泡从头顶冒出(大泡 ○ 先行、小泡 ∘ 跟上)向右飘走。
|
|
63
|
+
'whale-bubbles': {
|
|
64
|
+
frames: ['🐳 ', '🐳○ ', '🐳 ○', '🐳 ', '🐳∘ ', '🐳 ∘', '🐳 '],
|
|
65
|
+
intervalMs: 170,
|
|
66
|
+
},
|
|
67
|
+
// 时钟:12 个整点表盘循环(🕛 → 🕚),emoji 彩色渲染(同 moon8,不带 \uFE0E)。
|
|
68
|
+
clock: {
|
|
69
|
+
frames: ['🕛', '🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚'],
|
|
70
|
+
intervalMs: 300,
|
|
71
|
+
},
|
|
72
|
+
// 红绿灯:🔴 → 🟡 → 🟢 循环,一轮 1.2s,节奏从容。
|
|
73
|
+
traffic_lights: {
|
|
74
|
+
frames: ['🔴', '🟡', '🟢'],
|
|
75
|
+
intervalMs: 400,
|
|
76
|
+
},
|
|
77
|
+
comet: {
|
|
78
|
+
frames: ['● ', ' ● ', ' ● ', ' ● ', ' ●', ' ● ', ' ● ', ' ● '],
|
|
79
|
+
intervalMs: 160,
|
|
80
|
+
},
|
|
81
|
+
breathe: { frames: ['▁', '▃', '▅', '▇', '▅', '▃'], intervalMs: 210 },
|
|
82
|
+
dots: { frames: ['⣾', '⣷', '⣯', '⣟', '⡿', '⢿', '⣻', '⣽'], intervalMs: 140 },
|
|
83
|
+
arrow: { frames: ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'], intervalMs: 160 },
|
|
84
|
+
spark: { frames: ['·', '∘', '°', '✧', '°', '∘'], intervalMs: 240 },
|
|
85
|
+
bar: { frames: ['▏', '▎', '▍', '▌', '▋', '▊', '▉', '█', '▉', '▊', '▋', '▌', '▍', '▎'], intervalMs: 120 },
|
|
86
|
+
braille: { frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'], intervalMs: 120 },
|
|
87
|
+
arc: { frames: ['◜', '◠', '◝', '◞', '◡', '◟'], intervalMs: 160 },
|
|
88
|
+
circle: { frames: ['◴', '◷', '◶', '◵'], intervalMs: 190 },
|
|
89
|
+
grow: { frames: ['.', 'o', 'O', '0', 'O', 'o'], intervalMs: 210 },
|
|
90
|
+
noise: { frames: ['▓', '▒', '░', '▒'], intervalMs: 160 },
|
|
91
|
+
bounce: { frames: ['⠁', '⠂', '⠄', '⡀', '⢀', '⠠', '⠐', '⠈'], intervalMs: 140 },
|
|
92
|
+
rainbow: {
|
|
93
|
+
frames: ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█', '▇', '▆', '▅', '▄', '▃', '▂'],
|
|
94
|
+
intervalMs: 120,
|
|
95
|
+
},
|
|
96
|
+
// 双端进度条往返(pi 版独有,补全并集)。
|
|
97
|
+
bar2: {
|
|
98
|
+
frames: [
|
|
99
|
+
'[ ]', '[= ]', '[== ]', '[=== ]', '[ ===]',
|
|
100
|
+
'[ ==]', '[ =]', '[ ]', '[ =]', '[ ==]',
|
|
101
|
+
'[ ===]', '[=== ]', '[== ]', '[= ]',
|
|
102
|
+
],
|
|
103
|
+
intervalMs: 140,
|
|
104
|
+
},
|
|
105
|
+
dqpb: { frames: ['d', 'q', 'p', 'b'], intervalMs: 210 },
|
|
106
|
+
toggle: { frames: ['⊶', '⊷'], intervalMs: 300 },
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The default preset (moon8: the smoothest of the emoji moons). */
|
|
110
|
+
export const DEFAULT_PRESET = 'moon8'
|
|
111
|
+
|
|
112
|
+
/** Every selectable preset name, `random` first (the pi selector order). */
|
|
113
|
+
export const PRESET_NAMES: readonly string[] = ['random', ...Object.keys(FRAME_PRESETS)]
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Whether `name` selects a known preset or `random`.
|
|
117
|
+
* @param name - Candidate preset name.
|
|
118
|
+
* @returns True when the name resolves to a preset.
|
|
119
|
+
*/
|
|
120
|
+
export function isPresetName(name: string): boolean {
|
|
121
|
+
return name === 'random' || Object.hasOwn(FRAME_PRESETS, name)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Resolve a preset name (`random` picks one per process).
|
|
126
|
+
* @param name - Preset name, or undefined for the default.
|
|
127
|
+
* @returns The matching preset; unknown or absent names fall back to the default.
|
|
128
|
+
*/
|
|
129
|
+
export function resolvePreset(name: string | undefined): FramePreset {
|
|
130
|
+
if (name === 'random') {
|
|
131
|
+
const names = Object.keys(FRAME_PRESETS)
|
|
132
|
+
// FRAME_PRESETS is non-empty by construction, so any random index is in
|
|
133
|
+
// range and the lookup always lands on a preset.
|
|
134
|
+
const pick = names[Math.floor(Math.random() * names.length)]
|
|
135
|
+
return FRAME_PRESETS[pick]
|
|
136
|
+
}
|
|
137
|
+
return FRAME_PRESETS[name ?? ''] ?? FRAME_PRESETS[DEFAULT_PRESET]
|
|
138
|
+
}
|