@deepseek-harness-tui/dsh-tui 0.7.1 → 0.7.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.
Files changed (84) hide show
  1. package/README.md +6 -4
  2. package/bin/dsh-tui.js +37 -1
  3. package/cordis.patch.yml +10 -0
  4. package/lib/types/commands.js +1 -1
  5. package/lib/types/components/LoadedContextPanel.d.ts.map +1 -1
  6. package/lib/types/components/LoadedContextPanel.js +1 -1
  7. package/lib/types/components/design-system/ThemedText.d.ts +2 -2
  8. package/lib/types/components/design-system/ThemedText.d.ts.map +1 -1
  9. package/lib/types/components/design-system/ThemedText.js +1 -3
  10. package/lib/types/components/sessions/SessionListRow.d.ts +26 -0
  11. package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -0
  12. package/lib/types/components/sessions/SessionListRow.js +35 -0
  13. package/lib/types/components/sessions/SessionPreview.d.ts +25 -0
  14. package/lib/types/components/sessions/SessionPreview.d.ts.map +1 -0
  15. package/lib/types/components/sessions/SessionPreview.js +60 -0
  16. package/lib/types/dsh-adapter/channel.d.ts +9 -4
  17. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  18. package/lib/types/dsh-adapter/channel.js +31 -73
  19. package/lib/types/dsh-adapter/compat/index.d.ts +1 -1
  20. package/lib/types/dsh-adapter/compat/index.d.ts.map +1 -1
  21. package/lib/types/dsh-adapter/compat/index.js +1 -1
  22. package/lib/types/dsh-adapter/compat/sessionLog.d.ts +8 -0
  23. package/lib/types/dsh-adapter/compat/sessionLog.d.ts.map +1 -1
  24. package/lib/types/dsh-adapter/compat/sessionLog.js +1 -1
  25. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  26. package/lib/types/dsh-adapter/index.js +10 -1
  27. package/lib/types/dsh-adapter/packaged-presets.d.ts +22 -0
  28. package/lib/types/dsh-adapter/packaged-presets.d.ts.map +1 -0
  29. package/lib/types/dsh-adapter/packaged-presets.js +90 -0
  30. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  31. package/lib/types/dsh-adapter/plugin.js +46 -4
  32. package/lib/types/dsh-adapter/presets.d.ts +16 -0
  33. package/lib/types/dsh-adapter/presets.d.ts.map +1 -1
  34. package/lib/types/dsh-adapter/presets.js +23 -0
  35. package/lib/types/dsh-adapter/sessions/digest.d.ts +29 -0
  36. package/lib/types/dsh-adapter/sessions/digest.d.ts.map +1 -0
  37. package/lib/types/dsh-adapter/sessions/digest.js +224 -0
  38. package/lib/types/dsh-adapter/sessions/frames.d.ts +100 -0
  39. package/lib/types/dsh-adapter/sessions/frames.d.ts.map +1 -0
  40. package/lib/types/dsh-adapter/sessions/frames.js +246 -0
  41. package/lib/types/dsh-adapter/sessions/header.d.ts +50 -0
  42. package/lib/types/dsh-adapter/sessions/header.d.ts.map +1 -0
  43. package/lib/types/dsh-adapter/sessions/header.js +62 -0
  44. package/lib/types/dsh-adapter/sessions/index.d.ts +17 -0
  45. package/lib/types/dsh-adapter/sessions/index.d.ts.map +1 -0
  46. package/lib/types/dsh-adapter/sessions/index.js +15 -0
  47. package/lib/types/dsh-adapter/sessions/list.d.ts +41 -0
  48. package/lib/types/dsh-adapter/sessions/list.d.ts.map +1 -0
  49. package/lib/types/dsh-adapter/sessions/list.js +213 -0
  50. package/lib/types/dsh-adapter/sessions/store.d.ts +48 -0
  51. package/lib/types/dsh-adapter/sessions/store.d.ts.map +1 -0
  52. package/lib/types/dsh-adapter/sessions/store.js +164 -0
  53. package/lib/types/dsh-adapter/sessions/types.d.ts +142 -0
  54. package/lib/types/dsh-adapter/sessions/types.d.ts.map +1 -0
  55. package/lib/types/dsh-adapter/sessions/types.js +14 -0
  56. package/lib/types/i18n.d.ts +118 -14
  57. package/lib/types/i18n.d.ts.map +1 -1
  58. package/lib/types/i18n.js +36 -6
  59. package/lib/types/screens/Chat.d.ts +3 -2
  60. package/lib/types/screens/Chat.d.ts.map +1 -1
  61. package/lib/types/screens/Chat.js +28 -144
  62. package/lib/types/screens/SessionBrowser.d.ts +37 -0
  63. package/lib/types/screens/SessionBrowser.d.ts.map +1 -0
  64. package/lib/types/screens/SessionBrowser.js +437 -0
  65. package/lib/types/sessionHistory.d.ts +0 -8
  66. package/lib/types/sessionHistory.d.ts.map +1 -1
  67. package/lib/types/sessions/format.d.ts +117 -0
  68. package/lib/types/sessions/format.d.ts.map +1 -0
  69. package/lib/types/sessions/format.js +244 -0
  70. package/lib/types/sessions/view.d.ts +125 -0
  71. package/lib/types/sessions/view.d.ts.map +1 -0
  72. package/lib/types/sessions/view.js +222 -0
  73. package/package.json +6 -2
  74. package/presets/liangshen/.dsh-tui-managed.json +5 -0
  75. package/presets/liangshen/agent.cordis.yml +389 -0
  76. package/presets/liangshen/compaction-epoch.mjs +81 -0
  77. package/presets/liangshen/custom-bash.mjs +126 -0
  78. package/presets/liangshen/instruction-hint.mjs +181 -0
  79. package/presets/liangshen/preset.yml +3 -0
  80. package/presets/liangshen/skill-search.mjs +142 -0
  81. package/presets/liangshen/tool-bootstrap.mjs +300 -0
  82. package/lib/types/components/ResumePicker.d.ts +0 -26
  83. package/lib/types/components/ResumePicker.d.ts.map +0 -1
  84. package/lib/types/components/ResumePicker.js +0 -50
@@ -0,0 +1,181 @@
1
+ /**
2
+ * instruction-hint — replace `dsh-agent-instructions`' full AGENTS.md/CLAUDE.md
3
+ * injection with a minimal "these files exist" hint.
4
+ *
5
+ * WHY: the full workspace-instruction digest is a large injected block. After
6
+ * the anchored bootstrap promotes, we want the model to KNOW the instruction
7
+ * files exist (so it reads them before acting) without dumping their content
8
+ * into every request. The model reads the files itself via the filesystem
9
+ * tools when it needs them.
10
+ *
11
+ * Behavior:
12
+ * - After the session records its first durable promotion signal
13
+ * (`promoteOn`, default `either`), ONE hint message is injected (once per
14
+ * session — durable event scan, resume-safe), listing which instruction
15
+ * files were found:
16
+ * - user-global: `$DSH_HOME/AGENTS.md`
17
+ * - project chain: AGENTS.md / CLAUDE.md / AGENTS.local.md / CLAUDE.local.md
18
+ * walking up from the session cwd to the project root (a directory
19
+ * containing `.git`, or the cwd itself).
20
+ * - The hint instructs the model to READ the files before acting when
21
+ * relevant, without embedding their content.
22
+ * - Files are probed via `ctx.fs` (the host filesystem seam); a missing fs
23
+ * service or an unreadable probe degrades to no hint (never throws).
24
+ * - Pre-promotion requests get NO hint (matches the anchored bootstrap).
25
+ *
26
+ * ROW ORDER: this plugin registers its `agent/pre-step` handler with
27
+ * `prepend: true` and after `tool-bootstrap`, so it runs inside the
28
+ * bootstrap's outermost strip — but it emits AFTER promotion, when the strip
29
+ * is inactive. The hint source kind is `instruction-hint`, which is NOT in
30
+ * `suppressedContextSources`, so it is never stripped.
31
+ */
32
+
33
+ import { createEpochPromotion } from './compaction-epoch.mjs'
34
+
35
+ /** Cordis plugin name used by loader diagnostics. */
36
+ export const name = 'instruction-hint'
37
+
38
+ /** Durable session event types that count as a promotion signal per mode. */
39
+ const PROMOTE_EVENTS = {
40
+ 'tool-call': ['tool/call'],
41
+ 'assistant-message': ['assistant/message'],
42
+ either: ['tool/call', 'assistant/message'],
43
+ }
44
+
45
+ /** Candidate file names, in probe order, for the project chain and user-global. */
46
+ const PROJECT_CANDIDATES = ['AGENTS.md', 'CLAUDE.md', 'AGENTS.local.md', 'CLAUDE.local.md']
47
+ const USER_GLOBAL_CANDIDATE = 'AGENTS.md'
48
+
49
+ function parsePromoteOn(value) {
50
+ if (value === undefined || value === 'either') return PROMOTE_EVENTS.either
51
+ if (value === 'tool-call' || value === 'assistant-message') return PROMOTE_EVENTS[value]
52
+ throw new TypeError(`${name}: promoteOn must be one of "tool-call", "assistant-message", "either"; got ${JSON.stringify(value)}`)
53
+ }
54
+
55
+ /** Find the project root: first ancestor containing any root marker (e.g. .git). */
56
+ async function findProjectRoot(fs, cwd, signal) {
57
+ let current = cwd
58
+ for (;;) {
59
+ for (const marker of ['.git', '.hg', '.svn']) {
60
+ try {
61
+ const target = await fs.resolve(joinPath(current, marker), { cwd, signal })
62
+ const info = await fs.stat(target, signal)
63
+ if (info !== undefined) return current
64
+ } catch {
65
+ // Probe failure = marker absent; continue.
66
+ }
67
+ }
68
+ const parent = parentPath(current)
69
+ if (parent === current || parent.length === 0) return cwd
70
+ current = parent
71
+ }
72
+ }
73
+
74
+ /** List instruction files present in one directory (project candidates). */
75
+ async function presentInDir(fs, dir, candidates, signal) {
76
+ const found = []
77
+ for (const candidate of candidates) {
78
+ try {
79
+ const target = await fs.resolve(joinPath(dir, candidate), { cwd: dir, signal })
80
+ const info = await fs.stat(target, signal)
81
+ if (info !== undefined && info.type === 'file') found.push(candidate)
82
+ } catch {
83
+ // Absent or unreadable — skip.
84
+ }
85
+ }
86
+ return found
87
+ }
88
+
89
+ /** Join one path segment onto a directory (platform-agnostic string join). */
90
+ function joinPath(dir, segment) {
91
+ if (dir.endsWith('/') || dir.endsWith('\\')) return dir + segment
92
+ const sep = dir.includes('\\') ? '\\' : '/'
93
+ return dir + sep + segment
94
+ }
95
+
96
+ /** Parent of an absolute Windows or POSIX path. */
97
+ function parentPath(path) {
98
+ const idx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
99
+ if (idx <= 0) return path
100
+ const parent = path.slice(0, idx)
101
+ return parent.length === 0 ? path : parent
102
+ }
103
+
104
+ /** Register the post-promotion instruction-hint injector. */
105
+ export function apply(ctx, config) {
106
+ const promoteEvents = parsePromoteOn(config.promoteOn)
107
+ if (config.includeSubagents !== undefined && typeof config.includeSubagents !== 'boolean') {
108
+ throw new TypeError(`${name}: includeSubagents must be a boolean`)
109
+ }
110
+ const promotion = createEpochPromotion(promoteEvents, { includeSubagents: config.includeSubagents === true })
111
+ ctx.on('session/event', (session, event) => promotion.observe(session, event))
112
+
113
+ /** Sessions that already received the hint. */
114
+ const hinted = new Set()
115
+ let warned = false
116
+ const warnOnce = (message) => {
117
+ if (warned) return
118
+ warned = true
119
+ try {
120
+ ctx.logger.warn(message)
121
+ } catch {
122
+ // Logger unavailable — the guard exists only to avoid spamming.
123
+ }
124
+ }
125
+
126
+ ctx.on('agent/pre-step', async ({ agent, signal }, next) => {
127
+ const decision = await next()
128
+ try {
129
+ if (promotion.status(agent).promoted !== true) return decision
130
+ const session = agent.session
131
+ if (session === undefined || hinted.has(session.id)) return decision
132
+ hinted.add(session.id)
133
+
134
+ const fs = ctx.get('fs')
135
+ if (fs === undefined) return decision
136
+ const cwd = session.header.cwd ?? process.cwd()
137
+
138
+ const projectFiles = []
139
+ const root = await findProjectRoot(fs, cwd, signal)
140
+ projectFiles.push(...await presentInDir(fs, root, PROJECT_CANDIDATES, signal))
141
+
142
+ const userGlobalFiles = []
143
+ try {
144
+ const dshHome = process.env.DSH_HOME ?? (process.env.USERPROFILE ? `${process.env.USERPROFILE}\\.dsh` : undefined)
145
+ if (dshHome !== undefined) {
146
+ userGlobalFiles.push(...await presentInDir(fs, dshHome, [USER_GLOBAL_CANDIDATE], signal))
147
+ }
148
+ } catch {
149
+ // Unreadable home probe — ignore.
150
+ }
151
+
152
+ const sections = []
153
+ if (projectFiles.length > 0) {
154
+ sections.push(`Workspace instruction files exist: ${projectFiles.join(', ')} (project root: ${root}).`)
155
+ }
156
+ if (userGlobalFiles.length > 0) {
157
+ sections.push(`A user-global instruction file exists: ${USER_GLOBAL_CANDIDATE}.`)
158
+ }
159
+ if (sections.length === 0) return decision
160
+
161
+ const text = [
162
+ ...sections,
163
+ 'Do NOT assume their content. When a task touches this workspace, read the relevant instruction files first and follow them.',
164
+ ].join(' ')
165
+
166
+ return {
167
+ ...decision,
168
+ messages: [...decision.messages, {
169
+ id: `instruction-hint-${session.id}`,
170
+ role: 'user',
171
+ content: [{ type: 'text', text }],
172
+ source: { kind: 'instruction-hint', form: 'hint' },
173
+ }],
174
+ }
175
+ } catch (error) {
176
+ // A hint bug must never hurt the session: skip the hint.
177
+ warnOnce(`${name}: hint injection failed, skipping: ${String((error && error.message) || error)}`)
178
+ return decision
179
+ }
180
+ }, { prepend: true })
181
+ }
@@ -0,0 +1,3 @@
1
+ name: 梁神模式
2
+ description: 主 Agent 与子 Agent 首轮均保持 Minimal 双工具,首次工具调用后开放完整目录,压缩后重新锚定。
3
+ order: 5
@@ -0,0 +1,142 @@
1
+ /**
2
+ * skill-search — on-demand skill discovery and loading, replacing
3
+ * `dsh-tool-skill`'s full-catalog injection.
4
+ *
5
+ * WHY: the available-skills reminder (`<available_skills>`, ~9KB with many
6
+ * skills) is injected into the first step by dsh-tool-skill and again after
7
+ * every promotion/compaction. That large injected block perturbs the
8
+ * trajectory (issue #6: 0/9 anchored with the catalog present vs ~81%
9
+ * without). We remove the catalog injection entirely and expose two small
10
+ * tools instead — the Claude tool-search pattern:
11
+ *
12
+ * - `skill_search` — list skills whose name/description match a query
13
+ * (summaries only, bounded; no bodies). The model discovers what exists
14
+ * without a 9KB dump.
15
+ * - `skill_load` — load ONE skill's full instructions by exact name and
16
+ * inject them for the NEXT request via `agent.inject` (the non-waking
17
+ * next-step inbox). The model (or the user) calls this only when the
18
+ * skill is actually needed.
19
+ *
20
+ * Discovery reads `ctx.skills` scoped to the calling agent, exactly like
21
+ * dsh-tool-skill. If skills are unavailable the tools answer with a short
22
+ * message instead of throwing.
23
+ *
24
+ * NOTE: this plugin REPLACES the `dsh-tool-skill` row in the composition —
25
+ * the composition must NOT mount both, or the catalog injection returns.
26
+ */
27
+
28
+ /** Cordis plugin name used by loader diagnostics. */
29
+ export const name = 'skill-search'
30
+
31
+ /** The agent, tools, and skills services must exist before these tools can register. */
32
+ export const inject = ['agents', 'tools', 'skills']
33
+
34
+ const MAX_RESULTS = 20
35
+
36
+ /** Minimal JSON schema compiler for tool parameters (zero dependencies). */
37
+ function toJsonSchema(spec) {
38
+ const properties = {}
39
+ const required = []
40
+ for (const [key, meta] of Object.entries(spec || {})) {
41
+ const prop = { type: meta.type }
42
+ if (meta.description) prop.description = meta.description
43
+ properties[key] = prop
44
+ if (meta.required) required.push(key)
45
+ }
46
+ return { type: 'object', properties, required, additionalProperties: false }
47
+ }
48
+
49
+ /** Register the two on-demand skill tools. */
50
+ export function apply(ctx) {
51
+ /** Normalize a query into lowercase tokens for simple substring matching. */
52
+ const tokens = (text) => (text || '').toLowerCase().split(/[^a-z0-9_-]+/).filter(Boolean)
53
+
54
+ ctx.tools.register({
55
+ name: 'skill_search',
56
+ description: 'Search the available skills by keyword and return matching skill names with short descriptions. This session keeps NO skill catalog in the prompt — if a task looks like it matches a skill (document conversion, image processing, game reviews, markdown, PDF, spreadsheets, …), call skill_search FIRST to find it, then skill_load to activate it. Do NOT assume skill names from memory.',
57
+ parameters: toJsonSchema({
58
+ query: { type: 'string', required: true, description: 'search keywords (e.g. "pdf", "obsidian", "game review")' },
59
+ }),
60
+ output: {
61
+ schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string' } }, required: ['text'] },
62
+ render: (_a, v) => [{ type: 'text', text: v.text }],
63
+ },
64
+ async execute(args, exec) {
65
+ const wanted = tokens(args.query)
66
+ const scope = exec?.agent ?? ctx
67
+ try {
68
+ const all = await ctx.skills.list({
69
+ scope,
70
+ cwd: exec?.agent?.session?.header?.cwd,
71
+ signal: exec?.signal,
72
+ })
73
+ const matches = all.filter((skill) => {
74
+ if (wanted.length === 0) return true
75
+ const haystack = tokens(`${skill.name} ${skill.description ?? ''} ${skill.whenToUse ?? ''}`).join(' ')
76
+ return wanted.every((token) => haystack.includes(token))
77
+ })
78
+ const head = matches.slice(0, MAX_RESULTS)
79
+ const lines = head.map((skill) => {
80
+ const desc = (skill.description || '').split('\n')[0]
81
+ return `- ${skill.name}: ${desc}`
82
+ })
83
+ if (lines.length === 0) return { text: `No skills match "${args.query}". Use skill_search with other keywords.` }
84
+ const extra = matches.length > MAX_RESULTS ? `\n…(${matches.length - MAX_RESULTS} more)` : ''
85
+ return { text: `Matching skills (${matches.length}):\n${lines.join('\n')}${extra}\n\nLoad one with skill_load (exact name).` }
86
+ } catch (error) {
87
+ return { text: `skill_search unavailable: ${String((error && error.message) || error)}` }
88
+ }
89
+ },
90
+ })
91
+
92
+ ctx.tools.register({
93
+ name: 'skill_load',
94
+ description: 'Load the full instructions of ONE skill by its exact name (from skill_search results) and inject them for the next request. Call this before acting on a task that matches the skill.',
95
+ parameters: toJsonSchema({
96
+ name: { type: 'string', required: true, description: 'exact skill name (kebab-case, from skill_search)' },
97
+ }),
98
+ output: {
99
+ schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string' } }, required: ['text'] },
100
+ render: (_a, v) => [{ type: 'text', text: v.text }],
101
+ },
102
+ async execute(args, exec) {
103
+ try {
104
+ const agent = exec?.agent
105
+ if (agent === undefined) return { text: 'skill_load requires an agent context.' }
106
+ const skill = await ctx.skills.get(args.name, {
107
+ scope: agent,
108
+ cwd: agent.session.header.cwd,
109
+ signal: exec?.signal,
110
+ })
111
+ if (skill === undefined) {
112
+ return { text: `No skill named "${args.name}". Run skill_search to list available skills.` }
113
+ }
114
+ const body = extractSkillBody(skill)
115
+ if (body.length === 0) {
116
+ return { text: `Skill "${args.name}" has no loadable body.` }
117
+ }
118
+ // Queue the skill content as a non-waking next-step context message,
119
+ // exactly like dsh-tool-skill's invocation injection.
120
+ agent.inject({
121
+ id: `skill-load-${args.name}-${Date.now()}`,
122
+ role: 'user',
123
+ content: [{ type: 'text', text: body }],
124
+ source: { kind: 'skill-invocation', name: args.name, form: 'instructions' },
125
+ })
126
+ return { text: `Skill "${args.name}" loaded; its instructions will be injected for the next request.` }
127
+ } catch (error) {
128
+ return { text: `skill_load failed: ${String((error && error.message) || error)}` }
129
+ }
130
+ },
131
+ })
132
+ }
133
+
134
+ /** Extract the model-facing body of a loaded skill definition. */
135
+ function extractSkillBody(skill) {
136
+ const content = skill?.content ?? skill?.instructions ?? skill?.body
137
+ if (typeof content === 'string') return content
138
+ if (Array.isArray(content)) {
139
+ return content.map((part) => (typeof part === 'string' ? part : JSON.stringify(part))).join('\n')
140
+ }
141
+ return ''
142
+ }
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Anchored tool bootstrap — keep the FIRST model request on the Minimal
3
+ * preset's REAL tool schema (persistent `bash` + `str_replace_editor`), free
4
+ * of auto-injected workspace/skill context, then expose the complete assembled
5
+ * catalog once the session has produced its first durable tool call.
6
+ *
7
+ * The phase is derived from durable session events, so resume and reload
8
+ * preserve it. This preset configures `promoteOn: 'tool-call'`: text-only
9
+ * replies keep the bootstrap catalog, while the request after the first tool
10
+ * call sees every assembled tool at once. Other promotion modes remain
11
+ * supported for derived presets.
12
+ *
13
+ * First-request conditions established by the reproduction work (issues #6
14
+ * and #11, 2026-08-15):
15
+ *
16
+ * 1. Tool schema. The API-visible first-request catalog decides whether the
17
+ * session anchors on the Minimal trajectory. At the adapter-default
18
+ * maxTokens (256000 on the official endpoint) the Minimal tool pair —
19
+ * persistent `bash` + `str_replace_editor` — anchored 5/5 runs with zero
20
+ * `let me` first-lines, while every standard-family schema (pwsh/read,
21
+ * pwsh only, sandboxed bash/read) fell into standard-like behavior
22
+ * (11/11). Bootstrap therefore exposes exactly the Minimal pair, not
23
+ * Standard's `pwsh`/`read`.
24
+ *
25
+ * 2. Output budget. On the official endpoint the first request's `max_tokens`
26
+ * also dominated the trajectory anchor at 1024 (`We need` style in 26/32
27
+ * runs against 0/5 at 256000, independent of tool descriptions). The
28
+ * Minimal tool schema, however, anchors at 256000 WITHOUT any cap, and the
29
+ * cap's delivery depends on the profile package's `prepareCall` behavior
30
+ * (it reaches the request on the 0.1.0-rc.5 source checkout; a prebuilt
31
+ * rc.6-reporting profile package observed in issue #11 overwrote it with
32
+ * `adapterDefaults.maxTokens`). `bootstrapMaxTokens` is therefore OPT-IN:
33
+ * leave it unset to run the Minimal schema at the adapter default, or set
34
+ * it to cap the first request. When set, the cap is stripped after
35
+ * promotion — the next request's seed proposal carries the previous
36
+ * header's maxTokens forward, so the release must be explicit.
37
+ *
38
+ * 3. Injected reminders. dsh-agent-instructions and dsh-tool-skill inject
39
+ * workspace instructions (AGENTS.md) and the skill catalog into the first
40
+ * step as user messages whenever such content exists. With the skill
41
+ * catalog present the anchor did not reproduce at all (0/9); without it
42
+ * the same request reproduces at ~81%. Both message kinds are therefore
43
+ * stripped during bootstrap and allowed again after promotion. The
44
+ * stripped set is configurable via `suppressedContextSources` (default
45
+ * `['skill-catalog', 'agent-instructions']`); an explicitly empty array
46
+ * disables the context filter while keeping the tool bootstrap. A
47
+ * user-initiated skill gesture (`skill-invocation`) is NOT in the default
48
+ * set: it is not an automatic injection, and stripping it would lose the
49
+ * skill content once the gesture scrolls out of the per-step claim.
50
+ *
51
+ * POST-PROMOTION: return the complete assembled catalog without a resident or
52
+ * discovery phase. This matches the two-stage harness experiment: exact
53
+ * Minimal schemas first, then the full capability surface in one transition.
54
+ *
55
+ * COMPACTION (local addition): a compaction rewrites the whole surface, so the
56
+ * first post-compaction request is a "second first request". Promotion is
57
+ * epoch-aware (see compaction-epoch.mjs): after `compaction/end` the session
58
+ * falls back to the controlled phase. With this preset's omitted
59
+ * `compactionTools`, that surface is the exact bootstrap pair until a NEW
60
+ * durable tool call exists past the boundary.
61
+ *
62
+ * Robustness:
63
+ * - Promotion decisions are memoized per session id for this process; the
64
+ * durable event scan runs once per session per process, then O(1).
65
+ * - `includeSubagents: true` applies the same anchor/promotion cycle to
66
+ * delegated agents instead of giving them the full catalog immediately.
67
+ * - A missing bootstrap tool degrades to the full catalog with a one-time
68
+ * warning instead of throwing, so a composition drift can never brick
69
+ * every request of a session.
70
+ * - The pre-step context filter degrades to "keep everything" on failure:
71
+ * a filter bug must never eat the user's context.
72
+ * - Invalid config (bad tool lists, unknown `promoteOn`, malformed
73
+ * `suppressedContextSources`, non-positive `bootstrapMaxTokens`) fails at
74
+ * apply time, i.e. at preset mount, where it is visible and fixable.
75
+ */
76
+
77
+ import { createEpochPromotion } from './compaction-epoch.mjs'
78
+
79
+ /** Cordis plugin name used by loader diagnostics. */
80
+ export const name = 'anchored-tool-bootstrap'
81
+
82
+ /**
83
+ * Deliberately NO inject list: the listeners only touch services at event
84
+ * time. Applying without an inject — combined with this row being FIRST in
85
+ * agent.cordis.yml — registers the plugin before dsh-agent-instructions and
86
+ * dsh-tool-skill, and waterfall after-next transforms apply in reverse
87
+ * registration order, so the first-request strip below is the LAST transform.
88
+ * With an inject here those plugins register first and re-inject their
89
+ * messages after the strip. The pre-step listener additionally registers with
90
+ * `prepend: true` so the strip stays the outermost transform even against
91
+ * host-plane listeners and future row reordering.
92
+ */
93
+ export const inject = []
94
+
95
+ /** Durable session event types that count as a promotion signal per mode. */
96
+ const PROMOTE_EVENTS = {
97
+ 'tool-call': ['tool/call'],
98
+ 'assistant-message': ['assistant/message'],
99
+ either: ['tool/call', 'assistant/message'],
100
+ }
101
+
102
+ /** Every config key this plugin accepts — anything else is a typo. */
103
+ const ALLOWED_KEYS = new Set(['bootstrapTools', 'promoteOn', 'bootstrapMaxTokens', 'suppressedContextSources', 'compactionTools', 'includeSubagents'])
104
+
105
+ /**
106
+ * Context sources stripped from the first request by default. Both are
107
+ * automatic `agent/pre-step` injections: the available-skills reminder
108
+ * (`skill-catalog`) and the AGENTS.md/CLAUDE.md workspace digest
109
+ * (`agent-instructions`). True Minimal mounts neither plugin.
110
+ */
111
+ const DEFAULT_SUPPRESSED_SOURCES = ['skill-catalog', 'agent-instructions']
112
+
113
+ /**
114
+ * The default first-request catalog: the OFFICIAL Minimal preset's exact tool
115
+ * pair — the persistent `bash` shell and `str_replace_editor`. Issue #11
116
+ * measured this schema anchoring 5/5 at the adapter-default maxTokens while
117
+ * every standard-family schema failed 11/11.
118
+ */
119
+ const DEFAULT_BOOTSTRAP_TOOLS = ['bash', 'str_replace_editor']
120
+
121
+ function stringList(value, field) {
122
+ if (!Array.isArray(value) || value.length === 0 || value.some((item) => typeof item !== 'string' || item.length === 0)) {
123
+ throw new TypeError(`${name}: ${field} must be a non-empty array of non-empty strings`)
124
+ }
125
+ return [...new Set(value)]
126
+ }
127
+
128
+ function stringListOrEmpty(value, field) {
129
+ if (value === undefined) return []
130
+ return stringList(value, field)
131
+ }
132
+
133
+ function parsePromoteOn(value) {
134
+ if (value === undefined || value === 'either') return PROMOTE_EVENTS.either
135
+ if (value === 'tool-call' || value === 'assistant-message') return PROMOTE_EVENTS[value]
136
+ throw new TypeError(`${name}: promoteOn must be one of "tool-call", "assistant-message", "either"; got ${JSON.stringify(value)}`)
137
+ }
138
+
139
+ /**
140
+ * Validate the suppressed context sources. Unlike the bootstrap tool lists,
141
+ * an explicitly empty array is meaningful: it disables the context filter
142
+ * while keeping the tool bootstrap.
143
+ */
144
+ function sourceList(value, field, fallback) {
145
+ if (value === undefined) return new Set(fallback)
146
+ if (!Array.isArray(value) || value.some((item) => typeof item !== 'string' || item.length === 0)) {
147
+ throw new TypeError(`${name}: ${field} must be an array of non-empty strings`)
148
+ }
149
+ return new Set(value)
150
+ }
151
+
152
+ /**
153
+ * Validate the optional first-request output cap. `undefined` means NO cap:
154
+ * the Minimal tool schema anchors at the adapter-default maxTokens, and the
155
+ * cap's delivery is profile-package dependent (see the header note), so it is
156
+ * opt-in rather than the default.
157
+ */
158
+ function optionalPositiveInt(value, field) {
159
+ if (value === undefined) return undefined
160
+ if (!Number.isSafeInteger(value) || value <= 0) {
161
+ throw new TypeError(`${name}: ${field} must be a positive safe integer`)
162
+ }
163
+ return value
164
+ }
165
+
166
+ function optionalBoolean(value, field) {
167
+ if (value === undefined) return false
168
+ if (typeof value !== 'boolean') throw new TypeError(`${name}: ${field} must be a boolean`)
169
+ return value
170
+ }
171
+
172
+ /** Register the per-session bootstrap filters. */
173
+ export function apply(ctx, config) {
174
+ const source = config === undefined ? {} : config
175
+ if (typeof source !== 'object' || source === null || Array.isArray(source)) {
176
+ throw new TypeError(`${name}: config must be an object`)
177
+ }
178
+ const unknown = Object.keys(source).filter((key) => !ALLOWED_KEYS.has(key))
179
+ if (unknown.length > 0) {
180
+ throw new TypeError(
181
+ `${name}: unknown config key(s) ${unknown.join(', ')} — allowed keys: ${[...ALLOWED_KEYS].sort().join(', ')}`,
182
+ )
183
+ }
184
+ const bootstrapTools = stringList(source.bootstrapTools, 'bootstrapTools')
185
+ const promoteEvents = parsePromoteOn(source.promoteOn)
186
+ const bootstrapMaxTokens = optionalPositiveInt(source.bootstrapMaxTokens, 'bootstrapMaxTokens')
187
+ const includeSubagents = optionalBoolean(source.includeSubagents, 'includeSubagents')
188
+ const suppressedSources = sourceList(source.suppressedContextSources, 'suppressedContextSources', DEFAULT_SUPPRESSED_SOURCES)
189
+ // Core work set exposed after a compaction, before re-promotion. Empty
190
+ // means "no compaction recovery catalog": the session stays on the
191
+ // bootstrap pair until a new promotion signal.
192
+ const compactionTools = stringListOrEmpty(source.compactionTools, 'compactionTools')
193
+
194
+ const promotion = createEpochPromotion(promoteEvents, { includeSubagents })
195
+ ctx.on('session/event', (session, event) => promotion.observe(session, event))
196
+
197
+ let warned = false
198
+ const warnOnce = (message) => {
199
+ if (warned) return
200
+ warned = true
201
+ try {
202
+ ctx.logger.warn(message)
203
+ } catch {
204
+ // Logger unavailable — the guard exists only to avoid spamming.
205
+ }
206
+ }
207
+
208
+ /** Narrow the assembled catalog to a keep-set; validate required names. */
209
+ const keepTools = (assembled, keep, missingAllowsFullCatalog) => {
210
+ const available = new Set(assembled.tools.map((tool) => tool.name))
211
+ const missing = [...keep].filter((toolName) => !available.has(toolName))
212
+ if (missing.length > 0) {
213
+ warnOnce(
214
+ `${name}: expected every phase tool; missing=${JSON.stringify(missing)} — `
215
+ + (missingAllowsFullCatalog ? 'bootstrap disabled, full catalog exposed' : 'continuing with what is available'),
216
+ )
217
+ if (missingAllowsFullCatalog) return assembled
218
+ }
219
+ return {
220
+ ...assembled,
221
+ tools: assembled.tools.filter((tool) => keep.has(tool.name)),
222
+ }
223
+ }
224
+
225
+ ctx.on('system-prompt/assemble', async (_assembly, context, next) => {
226
+ // Downstream errors propagate untouched; only this filter's own logic is guarded.
227
+ const assembled = await next()
228
+ try {
229
+ const status = promotion.status(context.agent)
230
+ if (status.promoted) {
231
+ // PROMOTED: unlock the complete assembled catalog in one step.
232
+ return assembled
233
+ }
234
+ // Controlled phase: the bootstrap pair; after a compaction, plus the
235
+ // compaction work set so mid-task work can continue.
236
+ const { boundary } = status
237
+ const keep = new Set(bootstrapTools)
238
+ if (boundary >= 0) for (const toolName of compactionTools) keep.add(toolName)
239
+ return keepTools(assembled, keep, true)
240
+ } catch (error) {
241
+ // A filter bug must never brick a session: degrade to the full catalog.
242
+ warnOnce(`${name}: bootstrap filter failed, exposing the full catalog: ${String((error && error.message) || error)}`)
243
+ return assembled
244
+ }
245
+ })
246
+
247
+ // Optionally cap the first model request's output budget while bootstrapping.
248
+ // Unset (`bootstrapMaxTokens` omitted) means the adapter default flows — the
249
+ // Minimal tool schema anchors at 256000 without a cap (issue #11).
250
+ if (bootstrapMaxTokens !== undefined) {
251
+ // Same registration discipline as the pre-step strip below: `prepend`
252
+ // keeps this listener the OUTERMOST transform of the agent/request
253
+ // waterfall for the same registration-order reasons (loader row
254
+ // application is concurrent; row order alone does not decide listener
255
+ // order — see issue #6 and upstream PR #13), so a later listener can
256
+ // never override the first-round budget after we set it.
257
+ ctx.on('agent/request', async (payload, next) => {
258
+ const resolved = await next()
259
+ const agent = payload.agent
260
+ if (promotion.status(agent).promoted) {
261
+ // The next request's seed proposal carries the previous header's
262
+ // maxTokens forward, so the injected cap must be stripped explicitly —
263
+ // otherwise it would persist for the whole session.
264
+ if (resolved.maxTokens === bootstrapMaxTokens) {
265
+ const { maxTokens: _bootstrap, ...rest } = resolved
266
+ return rest
267
+ }
268
+ return resolved
269
+ }
270
+ return {
271
+ ...resolved,
272
+ maxTokens: bootstrapMaxTokens,
273
+ }
274
+ }, { prepend: true })
275
+ }
276
+
277
+ // Strip first-step injected reminders (skill catalog, AGENTS.md) during
278
+ // bootstrap. Because this listener is the first registered (see the inject
279
+ // note, the row order in agent.cordis.yml, and `prepend` below), the strip
280
+ // is the final waterfall transform and actually removes what later
281
+ // listeners inject.
282
+ ctx.on('agent/pre-step', async ({ agent }, next) => {
283
+ // Downstream errors propagate untouched; only this filter's own logic is guarded.
284
+ const decision = await next()
285
+ if (decision.kind === 'reject') return decision
286
+ try {
287
+ if (promotion.status(agent).promoted || suppressedSources.size === 0) return decision
288
+ if (!Array.isArray(decision.messages)) return decision
289
+ const kept = decision.messages.filter((message) => {
290
+ const kind = message?.source?.kind
291
+ return typeof kind !== 'string' || !suppressedSources.has(kind)
292
+ })
293
+ return kept.length === decision.messages.length ? decision : { ...decision, messages: kept }
294
+ } catch (error) {
295
+ // A filter bug must never eat context: degrade to keeping every message.
296
+ warnOnce(`${name}: pre-step context filter failed, keeping injected context: ${String((error && error.message) || error)}`)
297
+ return decision
298
+ }
299
+ }, { prepend: true })
300
+ }
@@ -1,26 +0,0 @@
1
- import React from 'react';
2
- import type { SessionRecord } from '../sessionHistory.js';
3
- /** Interaction mode of the `/resume` picker (issue #112). */
4
- export type ResumePickerMode = 'list' | 'confirm-delete' | 'rename';
5
- /**
6
- * `/resume` session picker in the CC ModelPicker style: a Pane with the
7
- * recent sessions as Select rows (title + time description, ✓ on the
8
- * current session), plus the hint line. Only WINDOW rows render; the window
9
- * follows the focused row, with `↑ N more` / `↓ N more` markers at the
10
- * edges.
11
- *
12
- * Beyond plain selection (pi-tui style session management, issue #112):
13
- * - `confirm-delete` replaces the hints with a delete confirmation for the
14
- * focused session (Enter deletes, Esc backs out);
15
- * - `rename` shows an inline SearchBox editing the focused session's title
16
- * (Enter saves, Esc discards).
17
- * Keyboard handling lives in the caller (Chat).
18
- */
19
- export declare function ResumePicker({ sessions, focusIndex, currentSessionId, mode, renameText, }: {
20
- sessions: readonly SessionRecord[];
21
- focusIndex: number;
22
- currentSessionId: string;
23
- mode: ResumePickerMode;
24
- renameText: string;
25
- }): React.ReactNode;
26
- //# sourceMappingURL=ResumePicker.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ResumePicker.d.ts","sourceRoot":"","sources":["../../../src/components/ResumePicker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAuBzD,6DAA6D;AAC7D,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,gBAAgB,GAAG,QAAQ,CAAA;AAEnE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,UAAU,EACV,gBAAgB,EAChB,IAAI,EACJ,UAAU,GACX,EAAE;IACD,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAA;IAClC,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,EAAE,MAAM,CAAA;IACxB,IAAI,EAAE,gBAAgB,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;CACnB,GAAG,KAAK,CAAC,SAAS,CAsElB"}