kc-beta 0.3.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agent/confidence-scorer.js +8 -0
- package/src/agent/context-window.js +7 -2
- package/src/agent/context.js +25 -0
- package/src/agent/corner-case-registry.js +5 -0
- package/src/agent/engine.js +564 -76
- package/src/agent/event-log.js +15 -2
- package/src/agent/history.js +91 -23
- package/src/agent/pipelines/initializer.js +3 -6
- package/src/agent/retry.js +9 -1
- package/src/agent/rule-catalog-normalize.js +37 -0
- package/src/agent/scheduler.js +276 -0
- package/src/agent/session-state.js +11 -2
- package/src/agent/task-manager.js +5 -0
- package/src/agent/tools/agent-tool.js +57 -14
- package/src/agent/tools/archive-file.js +94 -0
- package/src/agent/tools/copy-to-workspace.js +140 -0
- package/src/agent/tools/phase-advance.js +60 -0
- package/src/agent/tools/release.js +323 -0
- package/src/agent/tools/rule-catalog.js +56 -4
- package/src/agent/tools/schedule-fetch.js +118 -0
- package/src/agent/tools/snapshot.js +101 -0
- package/src/agent/tools/workspace-file.js +10 -7
- package/src/agent/version-manager.js +29 -120
- package/src/agent/workspace.js +127 -4
- package/src/cli/components.js +68 -12
- package/src/cli/index.js +147 -15
- package/src/config.js +10 -1
- package/src/model-tiers.json +5 -5
- package/template/release-runtime/README.md.tmpl +84 -0
- package/template/release-runtime/kc_runtime/__init__.py +2 -0
- package/template/release-runtime/kc_runtime/confidence.py +93 -0
- package/template/release-runtime/kc_runtime/dashboard.py +208 -0
- package/template/release-runtime/render_dashboard.py +49 -0
- package/template/release-runtime/run.py +230 -0
- package/template/release-runtime/serve.sh +15 -0
- package/template/skills/en/meta-meta/bootstrap-workspace/SKILL.md +11 -0
- package/template/skills/en/meta-meta/quality-control/SKILL.md +13 -1
- package/template/skills/en/meta-meta/skill-to-workflow/SKILL.md +8 -0
- package/template/skills/en/meta-meta/task-decomposition/SKILL.md +13 -0
- package/template/skills/en/meta-meta/version-control/SKILL.md +13 -0
- package/template/skills/zh/meta-meta/bootstrap-workspace/SKILL.md +11 -0
- package/template/skills/zh/meta-meta/quality-control/SKILL.md +12 -0
- package/template/skills/zh/meta-meta/skill-to-workflow/SKILL.md +8 -0
- package/template/skills/zh/meta-meta/task-decomposition/SKILL.md +16 -0
- package/template/skills/zh/meta-meta/version-control/SKILL.md +13 -0
- package/template/workspace.gitignore +22 -0
|
@@ -1,130 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Trace ID utility.
|
|
3
|
+
*
|
|
4
|
+
* As of v0.4.0 (Block 11), real version history is kept by git
|
|
5
|
+
* (per-session repo, auto-committed by Workspace.autoCommit). This module
|
|
6
|
+
* is now just a stable place for trace ID generation, used by tools that
|
|
7
|
+
* need to cross-reference a write or a result with the event log.
|
|
8
|
+
*
|
|
9
|
+
* The legacy versions.json manifest in pre-v0.4.0 workspaces is left
|
|
10
|
+
* untouched — nothing reads it any more, but old data is preserved.
|
|
11
|
+
*/
|
|
3
12
|
|
|
4
|
-
|
|
5
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Generate a trace ID like "20260417_114203_R001_workflow_result".
|
|
15
|
+
* @param {string} ruleId
|
|
16
|
+
* @param {string} [label]
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
export function generateTraceId(ruleId, label = "") {
|
|
20
|
+
const now = new Date().toISOString().replace(/[-:T]/g, (m) =>
|
|
21
|
+
m === "T" ? "_" : ""
|
|
22
|
+
).slice(0, 15);
|
|
23
|
+
const suffix = label ? `_${label}` : "";
|
|
24
|
+
return `${now}_${ruleId}${suffix}`;
|
|
25
|
+
}
|
|
6
26
|
|
|
7
27
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* - Manifest at versions.json: tracks lineage, timestamps, change reasons
|
|
11
|
-
* - Trace ID generation: {timestamp}_{rule_id}_{version}
|
|
12
|
-
* Cannot be bypassed — hooks into WorkspaceFileTool.
|
|
28
|
+
* Back-compat shell. The class is retained so existing constructors
|
|
29
|
+
* that take a VersionManager don't break, but it carries no state.
|
|
13
30
|
*/
|
|
14
31
|
export class VersionManager {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*/
|
|
18
|
-
constructor(workspacePath) {
|
|
19
|
-
this._workspace = workspacePath;
|
|
20
|
-
this._manifestPath = path.join(workspacePath, "versions.json");
|
|
21
|
-
this._manifest = this._loadManifest();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
_loadManifest() {
|
|
25
|
-
if (fs.existsSync(this._manifestPath)) {
|
|
26
|
-
try {
|
|
27
|
-
return JSON.parse(fs.readFileSync(this._manifestPath, "utf-8"));
|
|
28
|
-
} catch {
|
|
29
|
-
// fall through
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return { version: "0.1.0", entries: [] };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
_saveManifest() {
|
|
36
|
-
fs.writeFileSync(
|
|
37
|
-
this._manifestPath,
|
|
38
|
-
JSON.stringify(this._manifest, null, 2),
|
|
39
|
-
"utf-8",
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Check if a path falls within versioned directories.
|
|
45
|
-
* @param {string} relPath
|
|
46
|
-
* @returns {boolean}
|
|
47
|
-
*/
|
|
48
|
-
shouldVersion(relPath) {
|
|
49
|
-
const parts = relPath.split(path.sep);
|
|
50
|
-
if (parts.length === 0) return false;
|
|
51
|
-
const topDir = parts[0];
|
|
52
|
-
const ext = path.extname(relPath);
|
|
53
|
-
return VERSIONED_DIRS.has(topDir) && VERSIONED_EXTS.has(ext);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Called on file write. Returns trace ID if versioned, null otherwise.
|
|
58
|
-
* @param {string} relPath
|
|
59
|
-
* @param {string} content
|
|
60
|
-
* @returns {string|null}
|
|
61
|
-
*/
|
|
62
|
-
onWrite(relPath, content) {
|
|
63
|
-
if (!this.shouldVersion(relPath)) return null;
|
|
64
|
-
|
|
65
|
-
const version = this._nextVersion(relPath);
|
|
66
|
-
const now = new Date().toISOString().replace(/[-:T]/g, (m) =>
|
|
67
|
-
m === "T" ? "_" : ""
|
|
68
|
-
).slice(0, 15);
|
|
69
|
-
|
|
70
|
-
// Extract rule_id from path if possible (e.g., rule_skills/R001/SKILL.md → R001)
|
|
71
|
-
const parts = relPath.split(path.sep);
|
|
72
|
-
const ruleId = parts.length > 1 ? parts[1] : "global";
|
|
73
|
-
|
|
74
|
-
const traceId = `${now}_${ruleId}_v${version}`;
|
|
75
|
-
|
|
76
|
-
const entry = {
|
|
77
|
-
file: relPath,
|
|
78
|
-
version,
|
|
79
|
-
trace_id: traceId,
|
|
80
|
-
timestamp: new Date().toISOString(),
|
|
81
|
-
rule_id: ruleId,
|
|
82
|
-
size_chars: content.length,
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
this._manifest.entries.push(entry);
|
|
86
|
-
this._saveManifest();
|
|
87
|
-
|
|
88
|
-
return traceId;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
_nextVersion(relPath) {
|
|
92
|
-
const existing = this._manifest.entries
|
|
93
|
-
.filter((e) => e.file === relPath)
|
|
94
|
-
.map((e) => e.version);
|
|
95
|
-
return Math.max(0, ...existing) + 1;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Get all version entries for a file.
|
|
100
|
-
* @param {string} relPath
|
|
101
|
-
* @returns {Array<object>}
|
|
102
|
-
*/
|
|
103
|
-
getVersions(relPath) {
|
|
104
|
-
return this._manifest.entries.filter((e) => e.file === relPath);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Get the most recent version entry for a file.
|
|
109
|
-
* @param {string} relPath
|
|
110
|
-
* @returns {object|null}
|
|
111
|
-
*/
|
|
112
|
-
latestVersion(relPath) {
|
|
113
|
-
const versions = this.getVersions(relPath);
|
|
114
|
-
return versions.length > 0 ? versions[versions.length - 1] : null;
|
|
32
|
+
constructor(_workspacePath) {
|
|
33
|
+
// No-op. Workspace path is no longer needed.
|
|
115
34
|
}
|
|
116
35
|
|
|
117
|
-
/**
|
|
118
|
-
* Generate a standalone trace ID for results, QC records, etc.
|
|
119
|
-
* @param {string} ruleId
|
|
120
|
-
* @param {string} [label]
|
|
121
|
-
* @returns {string}
|
|
122
|
-
*/
|
|
123
36
|
generateTraceId(ruleId, label = "") {
|
|
124
|
-
|
|
125
|
-
m === "T" ? "_" : ""
|
|
126
|
-
).slice(0, 15);
|
|
127
|
-
const suffix = label ? `_${label}` : "";
|
|
128
|
-
return `${now}_${ruleId}${suffix}`;
|
|
37
|
+
return generateTraceId(ruleId, label);
|
|
129
38
|
}
|
|
130
39
|
}
|
package/src/agent/workspace.js
CHANGED
|
@@ -1,24 +1,41 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { generateTraceId } from "./version-manager.js";
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const GITIGNORE_TEMPLATE = path.resolve(__dirname, "../../template/workspace.gitignore");
|
|
4
10
|
|
|
5
11
|
/**
|
|
6
12
|
* Per-session workspace directory with path traversal protection.
|
|
7
13
|
* Each agent session gets its own directory under the workspace root.
|
|
8
14
|
* All file operations by tools must go through resolvePath().
|
|
15
|
+
*
|
|
16
|
+
* As of v0.4.0 (Block 11): the workspace is also a git repo. Writes to
|
|
17
|
+
* non-gitignored paths are auto-committed via autoCommit() so KC has a
|
|
18
|
+
* real version history of its outputs.
|
|
9
19
|
*/
|
|
10
20
|
export class Workspace {
|
|
11
21
|
/**
|
|
12
22
|
* @param {string} root - Workspace root directory
|
|
13
23
|
* @param {string} [sessionId] - Session identifier (auto-generated if omitted)
|
|
14
24
|
* @param {string} [projectDir] - User's project directory (CWD at launch)
|
|
25
|
+
* @param {object} [opts]
|
|
26
|
+
* @param {boolean} [opts.gitAutoCommit=true] - If false, skip git init / auto-commit
|
|
15
27
|
*/
|
|
16
|
-
constructor(root, sessionId, projectDir) {
|
|
28
|
+
constructor(root, sessionId, projectDir, opts = {}) {
|
|
17
29
|
this.root = path.resolve(root);
|
|
18
30
|
this.sessionId = sessionId || crypto.randomUUID().replace(/-/g, "").slice(0, 12);
|
|
19
31
|
this.path = path.resolve(this.root, this.sessionId);
|
|
20
32
|
this.projectDir = projectDir ? path.resolve(projectDir) : null;
|
|
33
|
+
this._currentPhase = "bootstrap";
|
|
21
34
|
fs.mkdirSync(this.path, { recursive: true });
|
|
35
|
+
|
|
36
|
+
this._gitAutoCommitEnabled = opts.gitAutoCommit !== false;
|
|
37
|
+
this._gitAvailable = this._gitAutoCommitEnabled && Workspace.isGitInstalled();
|
|
38
|
+
if (this._gitAvailable) this._initGitRepo();
|
|
22
39
|
}
|
|
23
40
|
|
|
24
41
|
/** @returns {string} Current workspace directory */
|
|
@@ -26,6 +43,16 @@ export class Workspace {
|
|
|
26
43
|
return this.path;
|
|
27
44
|
}
|
|
28
45
|
|
|
46
|
+
/** @returns {boolean} Whether auto-commit is wired up for this session */
|
|
47
|
+
get gitAvailable() {
|
|
48
|
+
return this._gitAvailable;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Update the current phase (used in auto-commit messages). */
|
|
52
|
+
setPhase(phase) {
|
|
53
|
+
if (phase) this._currentPhase = phase;
|
|
54
|
+
}
|
|
55
|
+
|
|
29
56
|
/**
|
|
30
57
|
* Resolve a user-supplied relative path against the workspace.
|
|
31
58
|
* Rejects absolute paths and any path that escapes the workspace via .. or symlinks.
|
|
@@ -66,23 +93,62 @@ export class Workspace {
|
|
|
66
93
|
}
|
|
67
94
|
|
|
68
95
|
/**
|
|
69
|
-
*
|
|
96
|
+
* Auto-commit a workspace write. Silently no-ops if the path is gitignored,
|
|
97
|
+
* if there's nothing to commit, or if git isn't available. Returns the trace
|
|
98
|
+
* ID generated for this write (always returned, even if no commit happened,
|
|
99
|
+
* so callers can cross-reference with the event log).
|
|
100
|
+
*
|
|
101
|
+
* @param {string} relPath - workspace-relative path that was just written
|
|
102
|
+
* @param {string} [opLabel="update"] - short verb for the commit message
|
|
103
|
+
* @returns {string} trace id
|
|
104
|
+
*/
|
|
105
|
+
autoCommit(relPath, opLabel = "update") {
|
|
106
|
+
const ruleId = this._extractRuleId(relPath);
|
|
107
|
+
const traceId = generateTraceId(ruleId, opLabel);
|
|
108
|
+
|
|
109
|
+
if (!this._gitAvailable) return traceId;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const r = spawnSync("git", ["add", "--", relPath], {
|
|
113
|
+
cwd: this.path,
|
|
114
|
+
stdio: "ignore",
|
|
115
|
+
});
|
|
116
|
+
if (r.status !== 0) return traceId; // gitignored or other add error — skip commit
|
|
117
|
+
const msg = `[${this._currentPhase}] ${opLabel} ${relPath} [trace:${traceId}]`;
|
|
118
|
+
spawnSync("git", ["commit", "-m", msg, "--allow-empty-message"], {
|
|
119
|
+
cwd: this.path,
|
|
120
|
+
stdio: "ignore",
|
|
121
|
+
});
|
|
122
|
+
// Status doesn't matter — "nothing to commit" is fine.
|
|
123
|
+
} catch {
|
|
124
|
+
// Never let a git failure break a workspace write.
|
|
125
|
+
}
|
|
126
|
+
return traceId;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Rename the workspace folder. Returns `{ sessionId, oldCwd, newCwd }` so
|
|
131
|
+
* callers can cascade the new path to subsystems that captured cwd at
|
|
132
|
+
* construction (Bug 3).
|
|
70
133
|
* @param {string} newName
|
|
71
|
-
* @returns {string}
|
|
134
|
+
* @returns {{sessionId: string, oldCwd: string, newCwd: string, changed: boolean}}
|
|
72
135
|
*/
|
|
73
136
|
rename(newName) {
|
|
74
137
|
newName = newName.trim().replace(/ /g, "_").replace(/\//g, "_");
|
|
75
138
|
if (!newName) throw new Error("Name cannot be empty");
|
|
76
139
|
const newPath = path.join(this.root, newName);
|
|
140
|
+
const oldCwd = this.path;
|
|
77
141
|
if (fs.existsSync(newPath) && path.resolve(newPath) !== path.resolve(this.path)) {
|
|
78
142
|
throw new Error(`Session '${newName}' already exists`);
|
|
79
143
|
}
|
|
144
|
+
let changed = false;
|
|
80
145
|
if (path.resolve(newPath) !== path.resolve(this.path)) {
|
|
81
146
|
fs.renameSync(this.path, newPath);
|
|
82
147
|
this.path = path.resolve(newPath);
|
|
83
148
|
this.sessionId = newName;
|
|
149
|
+
changed = true;
|
|
84
150
|
}
|
|
85
|
-
return this.sessionId;
|
|
151
|
+
return { sessionId: this.sessionId, oldCwd, newCwd: this.path, changed };
|
|
86
152
|
}
|
|
87
153
|
|
|
88
154
|
/**
|
|
@@ -102,4 +168,61 @@ export class Workspace {
|
|
|
102
168
|
}
|
|
103
169
|
return sessions;
|
|
104
170
|
}
|
|
171
|
+
|
|
172
|
+
/** Probe whether the `git` executable is on PATH. Cached per process. */
|
|
173
|
+
static isGitInstalled() {
|
|
174
|
+
if (Workspace._gitProbeCache !== undefined) return Workspace._gitProbeCache;
|
|
175
|
+
try {
|
|
176
|
+
execFileSync("git", ["--version"], { stdio: "ignore" });
|
|
177
|
+
Workspace._gitProbeCache = true;
|
|
178
|
+
} catch {
|
|
179
|
+
Workspace._gitProbeCache = false;
|
|
180
|
+
}
|
|
181
|
+
return Workspace._gitProbeCache;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// --- private helpers ---
|
|
185
|
+
|
|
186
|
+
_initGitRepo() {
|
|
187
|
+
const gitDir = path.join(this.path, ".git");
|
|
188
|
+
const gitignorePath = path.join(this.path, ".gitignore");
|
|
189
|
+
const isFresh = !fs.existsSync(gitDir);
|
|
190
|
+
|
|
191
|
+
if (isFresh) {
|
|
192
|
+
try {
|
|
193
|
+
spawnSync("git", ["init", "--initial-branch=main"], { cwd: this.path, stdio: "ignore" });
|
|
194
|
+
// --initial-branch isn't supported on older git; fall back silently
|
|
195
|
+
if (!fs.existsSync(gitDir)) {
|
|
196
|
+
spawnSync("git", ["init"], { cwd: this.path, stdio: "ignore" });
|
|
197
|
+
}
|
|
198
|
+
// Local identity so commits don't depend on user's global config
|
|
199
|
+
spawnSync("git", ["config", "user.name", "kc-agent"], { cwd: this.path, stdio: "ignore" });
|
|
200
|
+
spawnSync("git", ["config", "user.email", "agent@kc.local"], { cwd: this.path, stdio: "ignore" });
|
|
201
|
+
} catch {
|
|
202
|
+
this._gitAvailable = false;
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Always ensure .gitignore is present (template may have evolved)
|
|
208
|
+
if (!fs.existsSync(gitignorePath) && fs.existsSync(GITIGNORE_TEMPLATE)) {
|
|
209
|
+
fs.copyFileSync(GITIGNORE_TEMPLATE, gitignorePath);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (isFresh) {
|
|
213
|
+
// Initial commit — captures whatever's already in the dir (for migrated workspaces)
|
|
214
|
+
spawnSync("git", ["add", "-A"], { cwd: this.path, stdio: "ignore" });
|
|
215
|
+
const msg = fs.existsSync(path.join(this.path, "AGENT.md"))
|
|
216
|
+
? `Migrated session ${this.sessionId} to git-tracked workspace`
|
|
217
|
+
: `Initialized session ${this.sessionId}`;
|
|
218
|
+
spawnSync("git", ["commit", "--allow-empty", "-m", msg], { cwd: this.path, stdio: "ignore" });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Extract rule ID from a path like rule_skills/R001/SKILL.md → "R001". */
|
|
223
|
+
_extractRuleId(relPath) {
|
|
224
|
+
const parts = relPath.split(path.sep);
|
|
225
|
+
if (parts.length >= 2 && /^R\d+/i.test(parts[1])) return parts[1];
|
|
226
|
+
return "global";
|
|
227
|
+
}
|
|
105
228
|
}
|
package/src/cli/components.js
CHANGED
|
@@ -42,12 +42,20 @@ export function StatusBar({ sessionId, phase, contextTokens, contextLimit }) {
|
|
|
42
42
|
? `${(contextLimit / 1000).toFixed(0)}k`
|
|
43
43
|
: `${contextLimit || 0}`;
|
|
44
44
|
|
|
45
|
+
// Soft-threshold hint — shows up before auto-windowing kicks in at ~70%
|
|
46
|
+
// so users know they can run /compact to reduce context more aggressively
|
|
47
|
+
// than windowing does. Red hint at 80%+ means it's time to compact NOW.
|
|
48
|
+
const compactHint = pct >= 80 ? " · 💾 /compact"
|
|
49
|
+
: pct >= 60 ? " · 💾 建议 /compact"
|
|
50
|
+
: "";
|
|
51
|
+
|
|
45
52
|
return h(Box, { marginTop: 0 },
|
|
46
53
|
h(Text, { dimColor: true }, " ⏵⏵ KC Agent CLI "),
|
|
47
54
|
h(Text, { dimColor: true }, sessionId ? `[${sessionId}]` : ""),
|
|
48
55
|
phase ? h(Text, { color: "cyan" }, ` ${phase.toUpperCase()}`) : null,
|
|
49
56
|
h(Text, { color: "green" }, " ● "),
|
|
50
57
|
h(Text, { color: ctxColor }, `CTX: ${ctxLabel}/${limitLabel} (${pct}%)`),
|
|
58
|
+
compactHint ? h(Text, { color: ctxColor }, compactHint) : null,
|
|
51
59
|
h(Text, { dimColor: true }, ` · ${LENAT_QUOTE}`),
|
|
52
60
|
);
|
|
53
61
|
}
|
|
@@ -88,7 +96,7 @@ export function TaskDashboard({ tasks, progress }) {
|
|
|
88
96
|
|
|
89
97
|
// --- Welcome banner ---
|
|
90
98
|
|
|
91
|
-
export function WelcomeBanner({ projectDir } = {}) {
|
|
99
|
+
export function WelcomeBanner({ projectDir, pendingInputCount = 0 } = {}) {
|
|
92
100
|
return h(Box, { flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: "gray", paddingLeft: 1, paddingRight: 1 },
|
|
93
101
|
h(Box, null,
|
|
94
102
|
h(Text, { bold: true }, "KC AGENT CLI"),
|
|
@@ -102,6 +110,9 @@ export function WelcomeBanner({ projectDir } = {}) {
|
|
|
102
110
|
h(Text, { color: "yellow", dimColor: true }, "KC has full read/write access to this directory. We recommend backing up important files."),
|
|
103
111
|
)
|
|
104
112
|
: null,
|
|
113
|
+
pendingInputCount > 0
|
|
114
|
+
? h(Text, { color: "cyan" }, `📥 ${pendingInputCount} new file(s) pending in input/ — run /schedule for details`)
|
|
115
|
+
: null,
|
|
105
116
|
h(Text, null, ""),
|
|
106
117
|
h(Text, { dimColor: true }, "Product of Memium / kitchen-engineer42"),
|
|
107
118
|
);
|
|
@@ -109,29 +120,74 @@ export function WelcomeBanner({ projectDir } = {}) {
|
|
|
109
120
|
|
|
110
121
|
// --- Tool block ---
|
|
111
122
|
|
|
112
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Tool-result block.
|
|
125
|
+
*
|
|
126
|
+
* Rendering modes:
|
|
127
|
+
* - isRunning → yellow border, no output (spinner shown elsewhere).
|
|
128
|
+
* - isError → red border, ALWAYS show full output (errors are short + critical).
|
|
129
|
+
* - isRecent: true → green border, show up to ~4 lines + "N lines hidden" footer.
|
|
130
|
+
* - isRecent: false → header only (header includes line count + byte count).
|
|
131
|
+
*
|
|
132
|
+
* The full output is always on disk in logs/events.jsonl. Keeping the Ink
|
|
133
|
+
* tree slim is what lets KC handle long sessions without OOM / typing lag.
|
|
134
|
+
*/
|
|
135
|
+
const RECENT_PREVIEW_LINES = 4;
|
|
136
|
+
|
|
137
|
+
export function ToolBlock({ name, input, output, isError, isRunning, isRecent = true }) {
|
|
113
138
|
const borderColor = isRunning ? "yellow" : isError ? "red" : "green";
|
|
139
|
+
const outStr = typeof output === "string" ? output : "";
|
|
140
|
+
const lines = outStr ? outStr.split("\n") : [];
|
|
141
|
+
const bytes = outStr.length;
|
|
142
|
+
|
|
143
|
+
const header = h(Box, null,
|
|
144
|
+
h(Text, { color: borderColor }, "┃ "),
|
|
145
|
+
h(Text, { dimColor: true }, name),
|
|
146
|
+
input ? h(Text, { dimColor: true }, ` ${JSON.stringify(input).slice(0, 120)}`) : null,
|
|
147
|
+
outStr && !isRunning
|
|
148
|
+
? h(Text, { dimColor: true }, ` (${lines.length} 行 / ${bytes} 字节)`)
|
|
149
|
+
: null,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Errors: always show in full (short + critical).
|
|
153
|
+
if (isError && outStr) {
|
|
154
|
+
return h(Box, { flexDirection: "column", marginLeft: 2 },
|
|
155
|
+
header,
|
|
156
|
+
h(Box, { flexDirection: "column" },
|
|
157
|
+
...lines.map((line, i) =>
|
|
158
|
+
h(Box, { key: i },
|
|
159
|
+
h(Text, { color: "red" }, "┃ "),
|
|
160
|
+
h(Text, { color: "red" }, line),
|
|
161
|
+
),
|
|
162
|
+
),
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Off-screen / not-recent: header only. Full output remains on disk.
|
|
168
|
+
if (!isRecent || !outStr) {
|
|
169
|
+
return h(Box, { marginLeft: 2 }, header);
|
|
170
|
+
}
|
|
114
171
|
|
|
172
|
+
// Recent + successful: show preview + truncation footer.
|
|
173
|
+
const previewLines = lines.slice(0, RECENT_PREVIEW_LINES);
|
|
174
|
+
const remaining = lines.length - previewLines.length;
|
|
115
175
|
return h(Box, { flexDirection: "column", marginLeft: 2 },
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
input ? h(Text, { dimColor: true }, ` ${JSON.stringify(input)}`) : null,
|
|
120
|
-
),
|
|
121
|
-
output ? h(Box, { flexDirection: "column" },
|
|
122
|
-
...output.split("\n").slice(0, 20).map((line, i) =>
|
|
176
|
+
header,
|
|
177
|
+
h(Box, { flexDirection: "column" },
|
|
178
|
+
...previewLines.map((line, i) =>
|
|
123
179
|
h(Box, { key: i },
|
|
124
180
|
h(Text, { color: borderColor }, "┃ "),
|
|
125
181
|
h(Text, null, line),
|
|
126
182
|
),
|
|
127
183
|
),
|
|
128
|
-
|
|
184
|
+
remaining > 0
|
|
129
185
|
? h(Box, null,
|
|
130
186
|
h(Text, { color: borderColor }, "┃ "),
|
|
131
|
-
h(Text, { dimColor: true },
|
|
187
|
+
h(Text, { dimColor: true }, `… ${remaining} 行已省略(在 logs/events.jsonl 中完整保留)`),
|
|
132
188
|
)
|
|
133
189
|
: null,
|
|
134
|
-
)
|
|
190
|
+
),
|
|
135
191
|
);
|
|
136
192
|
}
|
|
137
193
|
|