u-foo 3.0.9 → 3.0.11
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/README.md +25 -9
- package/README.zh-CN.md +22 -9
- package/dist/tui/darwin-arm64/ufoo-tui +0 -0
- package/dist/tui/darwin-x64/ufoo-tui +0 -0
- package/dist/tui/linux-arm64/ufoo-tui +0 -0
- package/dist/tui/linux-x64/ufoo-tui +0 -0
- package/package.json +12 -4
- package/scripts/pack-tui.js +112 -0
- package/scripts/postinstall.js +11 -0
- package/src/agents/activity/activityReconcile.js +106 -0
- package/src/agents/activity/activityStatePublisher.js +31 -2
- package/src/agents/activity/index.js +1 -0
- package/src/agents/launch/launcher.js +19 -0
- package/src/agents/launch/ptyRunner.js +20 -1
- package/src/app/chat/ChatController.js +433 -0
- package/src/app/chat/agentDirectory.js +63 -0
- package/src/app/chat/agentEnter.js +70 -0
- package/src/app/chat/agentIdentity.js +50 -0
- package/src/app/chat/bootstrap.js +66 -0
- package/src/app/chat/commandExecutor.js +108 -0
- package/src/app/chat/commands.js +38 -1
- package/src/app/chat/dashboardView.js +6 -2
- package/src/app/chat/historyStore.js +181 -0
- package/src/app/chat/index.js +14 -2
- package/src/app/chat/inputSubmitHandler.js +21 -7
- package/src/app/chat/ipcBuilders.js +52 -0
- package/src/app/chat/multiWindow/paneManager.js +10 -1
- package/src/app/chat/multiWindow/renderer.js +1 -1
- package/src/app/chat/multiWindow/vtFrame.js +93 -0
- package/src/app/chat/streamState.js +182 -0
- package/src/app/cli/features/doctor.js +22 -0
- package/src/code/UcodeController.js +156 -0
- package/src/code/context/planGraphService.js +4 -0
- package/src/code/repl.js +4 -3
- package/src/code/runtime/taskLoop.js +46 -50
- package/src/code/tui.js +13 -2
- package/src/code/ucodeSlashDispatch.js +241 -0
- package/src/coordination/bus/activate.js +3 -0
- package/src/runtime/contracts/schemas/ufoo-ui-v1/envelope.json +28 -0
- package/src/runtime/contracts/uiProtocol.js +190 -0
- package/src/ui/{ink/chatLogModel.js → chatLogModel.js} +2 -2
- package/src/ui/dashboardBridge.js +81 -0
- package/src/ui/format/index.js +2 -2
- package/src/ui/index.js +8 -4
- package/src/ui/multiPaneBusMirror.js +137 -0
- package/src/ui/multiWindowHandoff.js +232 -0
- package/src/ui/ptyHandoff.js +23 -0
- package/src/ui/rustChatHost.js +1520 -0
- package/src/ui/rustMultiSession.js +497 -0
- package/src/ui/rustUcodeHost.js +999 -0
- package/src/ui/scrollbackReplay.js +82 -0
- package/src/ui/settingsBridge.js +49 -0
- package/src/ui/toolMergeBridge.js +66 -0
- package/src/ui/tuiLauncher.js +105 -0
- package/src/ui/ucodeStatusLine.js +74 -0
- package/src/ui/uiHostServer.js +339 -0
- package/src/ui/MIGRATION.md +0 -334
- package/src/ui/ink/ChatApp.js +0 -4152
- package/src/ui/ink/DashboardBar.js +0 -691
- package/src/ui/ink/InkDemo.js +0 -96
- package/src/ui/ink/MultilineInput.js +0 -662
- package/src/ui/ink/UcodeApp.js +0 -1675
- package/src/ui/ink/agentMirror.js +0 -730
- package/src/ui/ink/chatReducer.js +0 -473
- package/src/ui/runInk.js +0 -66
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Phase 2 replay harness: apply a transcript event list to a pure reducer
|
|
5
|
+
* shape used by Rust scrollback (JS mirror for fixtures).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const DEFAULT_CAP = 4000;
|
|
9
|
+
|
|
10
|
+
function createScrollbackState({ cap = DEFAULT_CAP } = {}) {
|
|
11
|
+
return {
|
|
12
|
+
entries: [],
|
|
13
|
+
cap,
|
|
14
|
+
scrollOffset: 0,
|
|
15
|
+
followTail: true,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function applyScrollbackEvent(state, event) {
|
|
20
|
+
const next = {
|
|
21
|
+
...state,
|
|
22
|
+
entries: state.entries.slice(),
|
|
23
|
+
};
|
|
24
|
+
const name = event && event.name;
|
|
25
|
+
const payload = (event && event.payload) || {};
|
|
26
|
+
if (name === "transcript.reset" || name === "app.snapshot") {
|
|
27
|
+
if (name === "app.snapshot" && Array.isArray(payload.entries)) {
|
|
28
|
+
next.entries = payload.entries.map((row, i) => ({
|
|
29
|
+
id: row.id || `e-${i}`,
|
|
30
|
+
kind: row.kind || "system",
|
|
31
|
+
text: String(row.text || ""),
|
|
32
|
+
speaker: String(row.speaker || ""),
|
|
33
|
+
}));
|
|
34
|
+
} else if (name === "transcript.reset") {
|
|
35
|
+
next.entries = [];
|
|
36
|
+
}
|
|
37
|
+
next.scrollOffset = 0;
|
|
38
|
+
next.followTail = true;
|
|
39
|
+
} else if (name === "transcript.append") {
|
|
40
|
+
next.entries.push({
|
|
41
|
+
id: payload.id || `e-${next.entries.length}`,
|
|
42
|
+
kind: payload.kind || "system",
|
|
43
|
+
text: String(payload.text || ""),
|
|
44
|
+
speaker: String(payload.speaker || ""),
|
|
45
|
+
});
|
|
46
|
+
} else if (name === "stream.delta") {
|
|
47
|
+
const id = payload.id || "stream";
|
|
48
|
+
const idx = [...next.entries].reverse().findIndex((e) => e.id === id);
|
|
49
|
+
if (idx >= 0) {
|
|
50
|
+
const real = next.entries.length - 1 - idx;
|
|
51
|
+
next.entries[real] = {
|
|
52
|
+
...next.entries[real],
|
|
53
|
+
text: `${next.entries[real].text}${payload.text || payload.delta || ""}`,
|
|
54
|
+
};
|
|
55
|
+
} else {
|
|
56
|
+
next.entries.push({
|
|
57
|
+
id,
|
|
58
|
+
kind: "assistant",
|
|
59
|
+
text: String(payload.text || payload.delta || ""),
|
|
60
|
+
speaker: String(payload.speaker || ""),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
while (next.entries.length > next.cap) next.entries.shift();
|
|
65
|
+
if (next.followTail) next.scrollOffset = 0;
|
|
66
|
+
return next;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function replayScrollbackEvents(events, options = {}) {
|
|
70
|
+
let state = createScrollbackState(options);
|
|
71
|
+
for (const event of events || []) {
|
|
72
|
+
state = applyScrollbackEvent(state, event);
|
|
73
|
+
}
|
|
74
|
+
return state;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
DEFAULT_CAP,
|
|
79
|
+
createScrollbackState,
|
|
80
|
+
applyScrollbackEvent,
|
|
81
|
+
replayScrollbackEvents,
|
|
82
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared launch-mode / agent-provider options for Ink + Rust chat hosts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const MODE_OPTIONS = Object.freeze(["auto", "host", "terminal", "tmux", "internal"]);
|
|
8
|
+
|
|
9
|
+
const PROVIDER_OPTIONS = Object.freeze([
|
|
10
|
+
{ label: "codex", value: "codex-cli" },
|
|
11
|
+
{ label: "claude", value: "claude-cli" },
|
|
12
|
+
{ label: "agy", value: "agy-cli" },
|
|
13
|
+
{ label: "kimi", value: "kimi-cli" },
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function buildSettingsSnapshot(settings = {}) {
|
|
17
|
+
const { normalizeLaunchMode, normalizeAgentProvider } = require("../config");
|
|
18
|
+
const launchMode = normalizeLaunchMode(settings.launchMode || "auto");
|
|
19
|
+
const agentProvider = normalizeAgentProvider(settings.agentProvider || "codex-cli");
|
|
20
|
+
return {
|
|
21
|
+
launch_mode: launchMode,
|
|
22
|
+
agent_provider: agentProvider,
|
|
23
|
+
mode_options: MODE_OPTIONS.slice(),
|
|
24
|
+
provider_options: PROVIDER_OPTIONS.map((opt) => ({ ...opt })),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function applySettingsPatch(projectRoot, patch = {}) {
|
|
29
|
+
const { saveConfig, normalizeLaunchMode, normalizeAgentProvider } = require("../config");
|
|
30
|
+
const next = {};
|
|
31
|
+
if (patch.launch_mode != null || patch.launchMode != null) {
|
|
32
|
+
next.launchMode = normalizeLaunchMode(patch.launch_mode || patch.launchMode);
|
|
33
|
+
}
|
|
34
|
+
if (patch.agent_provider != null || patch.agentProvider != null) {
|
|
35
|
+
next.agentProvider = normalizeAgentProvider(patch.agent_provider || patch.agentProvider);
|
|
36
|
+
}
|
|
37
|
+
if (Object.keys(next).length === 0) {
|
|
38
|
+
return { ok: false, error: "empty settings patch" };
|
|
39
|
+
}
|
|
40
|
+
saveConfig(projectRoot, next);
|
|
41
|
+
return { ok: true, settings: buildSettingsSnapshot({ ...patch, ...next }) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
MODE_OPTIONS,
|
|
46
|
+
PROVIDER_OPTIONS,
|
|
47
|
+
buildSettingsSnapshot,
|
|
48
|
+
applySettingsPatch,
|
|
49
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared tool-merge → ufoo-ui tool.* event helpers for Rust hosts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fmt = require("./format");
|
|
8
|
+
|
|
9
|
+
const EXIT_SUSPEND = 75;
|
|
10
|
+
|
|
11
|
+
function createToolMergePublisher(publish) {
|
|
12
|
+
let merge = null;
|
|
13
|
+
let mergeId = 1;
|
|
14
|
+
let scope = 0;
|
|
15
|
+
|
|
16
|
+
function beginScope() {
|
|
17
|
+
flush();
|
|
18
|
+
scope += 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function flush() {
|
|
22
|
+
if (!merge || !Array.isArray(merge.entries) || merge.entries.length === 0) {
|
|
23
|
+
merge = null;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const summary = fmt.buildMergedToolSummaryText(merge.entries);
|
|
27
|
+
const detail = fmt.buildMergedToolExpandedLines(merge.entries).join("\n");
|
|
28
|
+
const row = typeof fmt.buildToolMergeRowText === "function"
|
|
29
|
+
? fmt.buildToolMergeRowText(merge.entries)
|
|
30
|
+
: (merge.entries.length >= 2
|
|
31
|
+
? `· ${summary} (Ctrl+O expand)`
|
|
32
|
+
: summary);
|
|
33
|
+
publish("tool.group", {
|
|
34
|
+
id: `tool-merge-${merge.id}`,
|
|
35
|
+
summary: row || summary,
|
|
36
|
+
detail,
|
|
37
|
+
expanded_text: detail,
|
|
38
|
+
count: merge.entries.length,
|
|
39
|
+
});
|
|
40
|
+
merge = null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function pushTool(entry = {}) {
|
|
44
|
+
merge = fmt.appendToolMergeEntry(merge, entry, scope, mergeId);
|
|
45
|
+
if (merge && merge.id) mergeId = Math.max(mergeId, Number(merge.id) + 1);
|
|
46
|
+
// Live collapsed summary while group grows.
|
|
47
|
+
if (merge) {
|
|
48
|
+
publish("tool.start", {
|
|
49
|
+
id: `tool-merge-${merge.id}`,
|
|
50
|
+
summary: fmt.buildMergedToolSummaryText(merge.entries),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
beginScope,
|
|
57
|
+
flush,
|
|
58
|
+
pushTool,
|
|
59
|
+
EXIT_SUSPEND,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
EXIT_SUSPEND,
|
|
65
|
+
createToolMergePublisher,
|
|
66
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve / spawn ufoo-tui. Terminal UI is Rust-only.
|
|
5
|
+
*
|
|
6
|
+
* UFOO_TUI=auto|rust → rust when binary+version probe OK, else error.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
const path = require("path");
|
|
11
|
+
const { spawnSync } = require("child_process");
|
|
12
|
+
|
|
13
|
+
const PROTOCOL = "ufoo-ui/1";
|
|
14
|
+
|
|
15
|
+
function normalizeTuiMode(value = process.env.UFOO_TUI) {
|
|
16
|
+
const raw = String(value || "auto").trim().toLowerCase();
|
|
17
|
+
if (raw === "rust") return "rust";
|
|
18
|
+
return "auto";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function candidateBinaryPaths() {
|
|
22
|
+
const out = [];
|
|
23
|
+
if (process.env.UFOO_TUI_BIN) {
|
|
24
|
+
out.push(String(process.env.UFOO_TUI_BIN));
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
const platform = `${process.platform}-${process.arch}`;
|
|
28
|
+
const root = path.resolve(__dirname, "../..");
|
|
29
|
+
out.push(path.join(root, "target/release/ufoo-tui"));
|
|
30
|
+
out.push(path.join(root, "target/debug/ufoo-tui"));
|
|
31
|
+
out.push(path.join(root, "crates/ufoo-tui/target/release/ufoo-tui"));
|
|
32
|
+
out.push(path.join(root, "crates/ufoo-tui/target/debug/ufoo-tui"));
|
|
33
|
+
out.push(path.join(root, "dist/tui", platform, "ufoo-tui"));
|
|
34
|
+
try {
|
|
35
|
+
const optional = require.resolve(`@u-foo/tui-${platform}/ufoo-tui`);
|
|
36
|
+
out.push(optional);
|
|
37
|
+
} catch {
|
|
38
|
+
// optional package not installed
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveUfooTuiBinary() {
|
|
44
|
+
for (const candidate of candidateBinaryPaths()) {
|
|
45
|
+
try {
|
|
46
|
+
if (candidate && fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
47
|
+
return candidate;
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// continue
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function probeBinaryVersion(binaryPath) {
|
|
57
|
+
try {
|
|
58
|
+
const result = spawnSync(binaryPath, ["--version"], {
|
|
59
|
+
encoding: "utf8",
|
|
60
|
+
timeout: 3000,
|
|
61
|
+
});
|
|
62
|
+
if (result.status !== 0) return null;
|
|
63
|
+
return String(result.stdout || result.stderr || "").trim();
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveTuiLaunchPlan({
|
|
70
|
+
mode = normalizeTuiMode(),
|
|
71
|
+
surface = "chat",
|
|
72
|
+
} = {}) {
|
|
73
|
+
const normalized = normalizeTuiMode(mode);
|
|
74
|
+
const binary = resolveUfooTuiBinary();
|
|
75
|
+
const version = binary ? probeBinaryVersion(binary) : null;
|
|
76
|
+
|
|
77
|
+
if (!binary || !version) {
|
|
78
|
+
return {
|
|
79
|
+
mode: "error",
|
|
80
|
+
binary,
|
|
81
|
+
version,
|
|
82
|
+
protocol: PROTOCOL,
|
|
83
|
+
reason: binary ? "version_probe_failed" : "binary_missing",
|
|
84
|
+
surface,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
mode: "rust",
|
|
90
|
+
binary,
|
|
91
|
+
version,
|
|
92
|
+
protocol: PROTOCOL,
|
|
93
|
+
reason: normalized === "rust" ? "forced_rust" : "auto_prefer_rust",
|
|
94
|
+
surface,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
PROTOCOL,
|
|
100
|
+
normalizeTuiMode,
|
|
101
|
+
resolveUfooTuiBinary,
|
|
102
|
+
probeBinaryVersion,
|
|
103
|
+
resolveTuiLaunchPlan,
|
|
104
|
+
candidateBinaryPaths,
|
|
105
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure ucode status-line helpers (shared by Rust host tests / plan UI).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fmt = require("./format");
|
|
8
|
+
|
|
9
|
+
function inferStatusType(text = "", requestedType = "") {
|
|
10
|
+
const type = String(requestedType || "").trim().toLowerCase();
|
|
11
|
+
if (type === "done" || type === "success" || type === "error" || type === "idle" || type === "none") {
|
|
12
|
+
return type;
|
|
13
|
+
}
|
|
14
|
+
const clean = String(text || "").trim();
|
|
15
|
+
if (/^[✗!]/.test(clean) || /\b(error|failed|failure)\b/i.test(clean) || /失败|错误/.test(clean)) return "error";
|
|
16
|
+
if (
|
|
17
|
+
/^[✓✔]/.test(clean) ||
|
|
18
|
+
/^(done|complete|completed|finished|success|succeeded|ready)\b/i.test(clean) ||
|
|
19
|
+
/\bdone\s*$/i.test(clean) ||
|
|
20
|
+
/完成|成功/.test(clean)
|
|
21
|
+
) return "done";
|
|
22
|
+
return type || "thinking";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function collapseThinkingTail(text, maxChars = 80) {
|
|
26
|
+
const collapsed = String(text || "").replace(/\s+/g, " ").trim();
|
|
27
|
+
const parsed = Number(maxChars);
|
|
28
|
+
const limit = Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 80;
|
|
29
|
+
if (!collapsed) return "";
|
|
30
|
+
|
|
31
|
+
let candidate = collapsed;
|
|
32
|
+
const boldParts = collapsed.match(/\*\*[^*]+\*\*/g);
|
|
33
|
+
if (boldParts && boldParts.length > 0) {
|
|
34
|
+
candidate = boldParts[boldParts.length - 1].replace(/\*/g, "").trim() || candidate;
|
|
35
|
+
} else {
|
|
36
|
+
const clauses = collapsed.split(/(?<=[.!?。!?])\s+/).map((part) => part.trim()).filter(Boolean);
|
|
37
|
+
if (clauses.length > 1) candidate = clauses[clauses.length - 1];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (candidate.length <= limit) return candidate;
|
|
41
|
+
return `…${candidate.slice(-(limit - 1))}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function computeStatusText(status, spinnerTick, backgroundSuffix = "", idlePlanHint = "") {
|
|
45
|
+
const message = String((status && status.message) || "");
|
|
46
|
+
const suffix = String(backgroundSuffix || "");
|
|
47
|
+
if (!message) {
|
|
48
|
+
const hint = String(idlePlanHint || "").trim();
|
|
49
|
+
return hint ? `UCODE · Ready · ${hint}${suffix}` : `UCODE · Ready${suffix}`;
|
|
50
|
+
}
|
|
51
|
+
const type = inferStatusType(message, status && status.type);
|
|
52
|
+
if (type === "done" || type === "success") {
|
|
53
|
+
const clean = message.trim();
|
|
54
|
+
return `${/^[✓✔]/.test(clean) ? clean : `✓ ${clean}`}${suffix}`;
|
|
55
|
+
}
|
|
56
|
+
if (type === "error") {
|
|
57
|
+
const clean = message.trim();
|
|
58
|
+
return `${/^[✗!]/.test(clean) ? clean : `✗ ${clean}`}${suffix}`;
|
|
59
|
+
}
|
|
60
|
+
if (type === "idle" || type === "none") return `${message.trim() || "UCODE · Ready"}${suffix}`;
|
|
61
|
+
const indicators = fmt.STATUS_INDICATORS[type] || fmt.STATUS_INDICATORS.thinking;
|
|
62
|
+
const indicator = indicators[Math.max(0, Math.floor(Number(spinnerTick) || 0)) % indicators.length];
|
|
63
|
+
const startedAt = Number.isFinite(status && status.startedAt) ? status.startedAt : 0;
|
|
64
|
+
const timerText = status && status.showTimer && startedAt
|
|
65
|
+
? ` (${fmt.formatPendingElapsed(Date.now() - startedAt)}, esc cancel)`
|
|
66
|
+
: "";
|
|
67
|
+
return `${indicator} ${message}${timerText}${suffix}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
inferStatusType,
|
|
72
|
+
collapseThinkingTail,
|
|
73
|
+
computeStatusText,
|
|
74
|
+
};
|