newmark-agent 0.3.10 → 0.3.12
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/config.example.json +6 -0
- package/dist/cli-commands.d.ts +7 -0
- package/dist/cli-commands.js +206 -15
- package/dist/cli-discovery.d.ts +15 -0
- package/dist/cli-discovery.js +182 -0
- package/dist/cli-help.d.ts +4 -0
- package/dist/cli-help.js +46 -0
- package/dist/conversation-utility-host.bundle.cjs +548 -137
- package/dist/core/agent.d.ts +18 -3
- package/dist/core/agent.js +236 -34
- package/dist/core/agentKernelRunner.js +72 -10
- package/dist/core/browserControl.d.ts +8 -0
- package/dist/core/browserUsePageAdapter.d.ts +3 -0
- package/dist/core/browserUsePageAdapter.js +19 -2
- package/dist/core/computerUseSession.d.ts +44 -0
- package/dist/core/computerUseSession.js +105 -0
- package/dist/core/config.d.ts +7 -2
- package/dist/core/config.js +24 -6
- package/dist/core/conversationKernel.d.ts +1 -0
- package/dist/core/conversationKernel.js +39 -1
- package/dist/core/electronBrowserUseHost.js +7 -0
- package/dist/core/electronUtilityAgentClient.js +84 -2
- package/dist/core/electronUtilityRuntimePool.d.ts +11 -0
- package/dist/core/electronUtilityRuntimePool.js +62 -0
- package/dist/core/flow-runner.js +1 -1
- package/dist/core/modelValidationStore.d.ts +4 -1
- package/dist/core/modelValidationStore.js +7 -1
- package/dist/core/utilityHostToolRouter.d.ts +7 -0
- package/dist/core/utilityHostToolRouter.js +25 -33
- package/dist/core/workspace.d.ts +6 -0
- package/dist/core/workspace.js +14 -0
- package/dist/core/wslAgentRuntimePool.d.ts +4 -0
- package/dist/core/wslAgentRuntimePool.js +56 -0
- package/dist/launcher.js +51 -10
- package/dist/llm/provider.d.ts +8 -5
- package/dist/llm/provider.js +85 -33
- package/dist/main.js +297 -61
- package/dist/preload.js +10 -2
- package/dist/providers/chat-completions.adapter.js +1 -5
- package/dist/providers/provider-events.d.ts +7 -0
- package/dist/providers/provider-events.js +44 -0
- package/dist/providers/responses.adapter.js +1 -3
- package/dist/tools/index.js +36 -49
- package/dist/tui/src/adapters/core-runtime-adapter.js +40 -3
- package/dist/tui/src/app.js +47 -13
- package/dist/tui/src/render.js +23 -7
- package/dist/tui/src/state.js +61 -9
- package/dist/ui/index.html +545 -122
- package/dist/wsl-agent-host.bundle.cjs +548 -137
- package/package.json +14 -5
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.defaultProviderTransport = defaultProviderTransport;
|
|
4
4
|
exports.providerAbortError = providerAbortError;
|
|
5
|
+
exports.providerStreamTimeoutError = providerStreamTimeoutError;
|
|
6
|
+
exports.readProviderStreamChunk = readProviderStreamChunk;
|
|
5
7
|
exports.parseProviderSse = parseProviderSse;
|
|
6
8
|
exports.isContentPolicyBlocked = isContentPolicyBlocked;
|
|
7
9
|
exports.normalizeProviderUsage = normalizeProviderUsage;
|
|
@@ -34,6 +36,48 @@ function providerAbortError(signal) {
|
|
|
34
36
|
error.name = 'AbortError';
|
|
35
37
|
return error;
|
|
36
38
|
}
|
|
39
|
+
function providerStreamTimeoutError(timeoutMs) {
|
|
40
|
+
const error = new Error('Stream read timeout');
|
|
41
|
+
error.name = 'TimeoutError';
|
|
42
|
+
error.message = `Stream read timeout after ${timeoutMs}ms`;
|
|
43
|
+
return error;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read one SSE chunk with both user cancellation and an inactivity deadline.
|
|
47
|
+
* Cancelling the reader is important: rejecting the race alone leaves the
|
|
48
|
+
* provider socket alive and lets later requests accumulate behind it.
|
|
49
|
+
*/
|
|
50
|
+
async function readProviderStreamChunk(reader, signal, timeoutMs = 30_000) {
|
|
51
|
+
if (signal.aborted)
|
|
52
|
+
throw providerAbortError(signal);
|
|
53
|
+
let timer;
|
|
54
|
+
let onAbort;
|
|
55
|
+
const abortPromise = new Promise((_, reject) => {
|
|
56
|
+
onAbort = () => reject(providerAbortError(signal));
|
|
57
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
58
|
+
});
|
|
59
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
60
|
+
timer = setTimeout(() => reject(providerStreamTimeoutError(timeoutMs)), timeoutMs);
|
|
61
|
+
});
|
|
62
|
+
try {
|
|
63
|
+
return await Promise.race([reader.read(), abortPromise, timeoutPromise]);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (signal.aborted || (error instanceof Error && error.name === 'TimeoutError')) {
|
|
67
|
+
try {
|
|
68
|
+
await reader.cancel(error);
|
|
69
|
+
}
|
|
70
|
+
catch { }
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
if (timer)
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
if (onAbort)
|
|
78
|
+
signal.removeEventListener('abort', onAbort);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
37
81
|
function parseProviderSse(raw) {
|
|
38
82
|
const events = [];
|
|
39
83
|
for (const block of String(raw || '').replace(/\r\n/g, '\n').split(/\n\n+/)) {
|
|
@@ -132,9 +132,7 @@ class ResponsesAdapter {
|
|
|
132
132
|
let streamError = '';
|
|
133
133
|
try {
|
|
134
134
|
while (true) {
|
|
135
|
-
|
|
136
|
-
throw (0, provider_events_1.providerAbortError)(signal);
|
|
137
|
-
const { done, value } = await reader.read();
|
|
135
|
+
const { done, value } = await (0, provider_events_1.readProviderStreamChunk)(reader, signal);
|
|
138
136
|
if (done)
|
|
139
137
|
break;
|
|
140
138
|
buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, '\n');
|
package/dist/tools/index.js
CHANGED
|
@@ -57,8 +57,7 @@ const nativeBash_1 = require("../core/nativeBash");
|
|
|
57
57
|
const toolArgumentValidator_1 = require("../core/toolArgumentValidator");
|
|
58
58
|
const localOcr_1 = require("../core/localOcr");
|
|
59
59
|
const visualTextFallback_1 = require("../core/visualTextFallback");
|
|
60
|
-
|
|
61
|
-
const COMPUTER_USE_LOCK_TTL_MS = 10 * 60 * 1000;
|
|
60
|
+
const computerUseSession_1 = require("../core/computerUseSession");
|
|
62
61
|
function normalizeComputerUseAction(action) {
|
|
63
62
|
return String(action || '').trim().toLowerCase();
|
|
64
63
|
}
|
|
@@ -154,50 +153,28 @@ async function abortableToolDelay(durationMs, signal) {
|
|
|
154
153
|
abort();
|
|
155
154
|
});
|
|
156
155
|
}
|
|
157
|
-
function
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
function computerUseLockError(action, owner) {
|
|
163
|
-
return JSON.stringify({
|
|
164
|
-
ok: false,
|
|
165
|
-
action,
|
|
166
|
-
error: `ComputerUse is already active in ${computerUseLock?.owner || 'another conversation'}. Stop it with computer_use takeover_stop or wait before using ComputerUse from another conversation.`,
|
|
167
|
-
lock_owner: computerUseLock?.owner || '',
|
|
168
|
-
requested_owner: owner,
|
|
169
|
-
}, null, 2);
|
|
170
|
-
}
|
|
171
|
-
function acquireComputerUseLock(action, owner, wsPath) {
|
|
172
|
-
const now = Date.now();
|
|
173
|
-
clearStaleComputerUseLock(now);
|
|
174
|
-
if (computerUseLock && computerUseLock.owner !== owner) {
|
|
175
|
-
return computerUseLockError(action, owner);
|
|
176
|
-
}
|
|
177
|
-
computerUseLock = {
|
|
178
|
-
owner,
|
|
156
|
+
function computerUseSessionScope(context, wsPath, owner) {
|
|
157
|
+
return {
|
|
158
|
+
runtimeKey: browserUseScope(context, wsPath).runtimeKey,
|
|
159
|
+
ownerLabel: owner,
|
|
179
160
|
workspacePath: path.resolve(wsPath || process.cwd()),
|
|
180
|
-
acquiredAt: computerUseLock?.owner === owner ? computerUseLock.acquiredAt : now,
|
|
181
|
-
updatedAt: now,
|
|
182
161
|
};
|
|
183
|
-
return null;
|
|
184
162
|
}
|
|
185
|
-
function
|
|
186
|
-
|
|
187
|
-
if (computerUseLock && computerUseLock.owner !== owner) {
|
|
188
|
-
return computerUseLockError(action, owner);
|
|
189
|
-
}
|
|
190
|
-
if (computerUseLock?.owner === owner)
|
|
191
|
-
computerUseLock = null;
|
|
192
|
-
return null;
|
|
163
|
+
function acquireComputerUseLock(action, owner, wsPath, context = {}, dryRun = false) {
|
|
164
|
+
return computerUseSession_1.defaultComputerUseSessionRegistry.authorize(action, computerUseSessionScope(context, wsPath, owner), dryRun);
|
|
193
165
|
}
|
|
194
|
-
function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return computerUseLockError(action, owner);
|
|
198
|
-
}
|
|
166
|
+
function releaseComputerUseLock(action, owner, context = {}, wsPath = context.workspacePath || '') {
|
|
167
|
+
const scope = computerUseSessionScope(context, wsPath, owner);
|
|
168
|
+
computerUseSession_1.defaultComputerUseSessionRegistry.complete(action, scope);
|
|
199
169
|
return null;
|
|
200
170
|
}
|
|
171
|
+
function assertComputerUseLockOwner(action, owner, context = {}, wsPath = context.workspacePath || '') {
|
|
172
|
+
return computerUseSession_1.defaultComputerUseSessionRegistry.authorize(action, computerUseSessionScope(context, wsPath, owner));
|
|
173
|
+
}
|
|
174
|
+
// A competing conversation receives the stable user-facing marker
|
|
175
|
+
// "ComputerUse is already active" / "computerUse occupied".
|
|
176
|
+
// The two-argument releaseComputerUseLock(action, owner) form remains valid
|
|
177
|
+
// for direct callers; routed Build calls additionally provide their target.
|
|
201
178
|
class ToolExecutor {
|
|
202
179
|
config;
|
|
203
180
|
ssh;
|
|
@@ -754,8 +731,8 @@ class ToolExecutor {
|
|
|
754
731
|
const action = normalizeComputerUseAction(g('action'));
|
|
755
732
|
const owner = `${computerUseOwner(context, wsPath)}:${String(context.actorId || 'root')}`;
|
|
756
733
|
const lockGuard = action === 'takeover_stop'
|
|
757
|
-
? assertComputerUseLockOwner(action, owner)
|
|
758
|
-
: acquireComputerUseLock(action, owner, wsPath);
|
|
734
|
+
? assertComputerUseLockOwner(action, owner, context, wsPath)
|
|
735
|
+
: acquireComputerUseLock(action, owner, wsPath, context, args.dry_run === true);
|
|
759
736
|
if (lockGuard)
|
|
760
737
|
return lockGuard;
|
|
761
738
|
if (process.env.NEWMARK_WSL_DISTRO) {
|
|
@@ -772,7 +749,7 @@ class ToolExecutor {
|
|
|
772
749
|
}
|
|
773
750
|
finally {
|
|
774
751
|
if (action === 'takeover_stop')
|
|
775
|
-
releaseComputerUseLock(action, owner);
|
|
752
|
+
releaseComputerUseLock(action, owner, context, wsPath);
|
|
776
753
|
}
|
|
777
754
|
}
|
|
778
755
|
if (process.env.NEWMARK_ISOLATED_RUNTIME === '1') {
|
|
@@ -794,7 +771,7 @@ class ToolExecutor {
|
|
|
794
771
|
}
|
|
795
772
|
finally {
|
|
796
773
|
if (action === 'takeover_stop')
|
|
797
|
-
releaseComputerUseLock(action, owner);
|
|
774
|
+
releaseComputerUseLock(action, owner, context, wsPath);
|
|
798
775
|
}
|
|
799
776
|
}
|
|
800
777
|
const output = await (0, computerUse_1.runComputerUse)({
|
|
@@ -838,7 +815,7 @@ class ToolExecutor {
|
|
|
838
815
|
: undefined,
|
|
839
816
|
});
|
|
840
817
|
if (action === 'takeover_stop')
|
|
841
|
-
releaseComputerUseLock(action, owner);
|
|
818
|
+
releaseComputerUseLock(action, owner, context, wsPath);
|
|
842
819
|
return output;
|
|
843
820
|
}
|
|
844
821
|
case 'terminal_takeover': {
|
|
@@ -1352,9 +1329,17 @@ class ToolExecutor {
|
|
|
1352
1329
|
}
|
|
1353
1330
|
}
|
|
1354
1331
|
async browserRun(request, signal, context = {}, workspacePath = this.root) {
|
|
1332
|
+
const scope = browserUseScope(context, workspacePath);
|
|
1333
|
+
const scopedRequest = {
|
|
1334
|
+
...request,
|
|
1335
|
+
target: {
|
|
1336
|
+
workspaceId: context.workspaceId || (0, terminalTakeover_1.terminalTakeoverWorkspaceId)(workspacePath),
|
|
1337
|
+
conversationId: context.conversationId || 'default',
|
|
1338
|
+
runtimeKey: scope.runtimeKey,
|
|
1339
|
+
},
|
|
1340
|
+
};
|
|
1355
1341
|
if (process.env.NEWMARK_WSL_DISTRO) {
|
|
1356
|
-
const
|
|
1357
|
-
const result = await (0, wslHostToolBridge_1.requestWindowsHostTool)('browser_control', request, {
|
|
1342
|
+
const result = await (0, wslHostToolBridge_1.requestWindowsHostTool)('browser_control', scopedRequest, {
|
|
1358
1343
|
conversationId: context.conversationId || process.env.NEWMARK_CONVERSATION_ID || 'default',
|
|
1359
1344
|
workspaceId: process.env.NEWMARK_WORKSPACE_ID || context.workspaceId || (0, terminalTakeover_1.terminalTakeoverWorkspaceId)(workspacePath),
|
|
1360
1345
|
actorId: context.actorId || terminalTakeover_1.ROOT_TERMINAL_ACTOR_ID,
|
|
@@ -1364,10 +1349,12 @@ class ToolExecutor {
|
|
|
1364
1349
|
return this.formatBrowserResult(result);
|
|
1365
1350
|
}
|
|
1366
1351
|
if (process.env.NEWMARK_ISOLATED_RUNTIME === '1') {
|
|
1367
|
-
const result = await (0, utilityHostToolBridge_1.requestUtilityHostTool)('browser_control',
|
|
1352
|
+
const result = await (0, utilityHostToolBridge_1.requestUtilityHostTool)('browser_control', scopedRequest, undefined, 30_000, signal);
|
|
1368
1353
|
return this.formatBrowserResult(result);
|
|
1369
1354
|
}
|
|
1370
|
-
|
|
1355
|
+
// Preserve the cancellation contract of BrowserControl.run(request, signal)
|
|
1356
|
+
// while routing the concrete request through the target-bound copy above.
|
|
1357
|
+
const result = await browserControl_1.BrowserControl.run(scopedRequest, signal);
|
|
1371
1358
|
return this.formatBrowserResult(result);
|
|
1372
1359
|
}
|
|
1373
1360
|
formatBrowserResult(result) {
|
|
@@ -93,6 +93,41 @@ function samePath(left, right) {
|
|
|
93
93
|
return normalize(left) === normalize(right);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
function isPathInside(parent, child) {
|
|
97
|
+
try {
|
|
98
|
+
const relative = path.relative(path.resolve(parent), path.resolve(child));
|
|
99
|
+
return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative));
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isProtectedInstallPath(candidate) {
|
|
106
|
+
if (process.platform !== "win32") return false;
|
|
107
|
+
const protectedRoots = [
|
|
108
|
+
process.env.ProgramFiles,
|
|
109
|
+
process.env["ProgramFiles(x86)"],
|
|
110
|
+
process.env.ProgramW6432
|
|
111
|
+
].filter(Boolean);
|
|
112
|
+
return protectedRoots.some((root) => isPathInside(root, candidate));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function safeWorkspacePath(root, candidate) {
|
|
116
|
+
const resolvedRoot = path.resolve(root);
|
|
117
|
+
const resolvedCandidate = path.resolve(candidate || resolvedRoot);
|
|
118
|
+
const executableRoot = path.dirname(process.execPath);
|
|
119
|
+
// A packaged TUI launched from its installation directory must never turn
|
|
120
|
+
// that directory into an external workspace. The runtime root owns the
|
|
121
|
+
// default internal workspace in this case.
|
|
122
|
+
if (
|
|
123
|
+
samePath(resolvedCandidate, resolvedRoot)
|
|
124
|
+
|| isPathInside(resolvedRoot, resolvedCandidate)
|
|
125
|
+
|| isPathInside(executableRoot, resolvedCandidate)
|
|
126
|
+
|| isProtectedInstallPath(resolvedCandidate)
|
|
127
|
+
) return resolvedRoot;
|
|
128
|
+
return resolvedCandidate;
|
|
129
|
+
}
|
|
130
|
+
|
|
96
131
|
function mergeProviderConfig(currentProviders, incomingProviders) {
|
|
97
132
|
return (incomingProviders || []).map((incoming) => {
|
|
98
133
|
const current = (currentProviders || []).find((provider) => provider.id === incoming.id);
|
|
@@ -120,7 +155,7 @@ function createCoreRuntimeAdapter(options = {}) {
|
|
|
120
155
|
const { FlowEngine } = require(path.join(desktopDist, "core", "flow.js"));
|
|
121
156
|
const installUpdate = require(path.join(desktopDist, "core", "installUpdate.js"));
|
|
122
157
|
const root = path.resolve(options.root || path.join(os.homedir(), ".Newmark"));
|
|
123
|
-
const workspacePath =
|
|
158
|
+
const workspacePath = safeWorkspacePath(root, options.workspacePath || process.cwd());
|
|
124
159
|
ensureRuntimeRoot(root, configModule);
|
|
125
160
|
|
|
126
161
|
const agent = new Agent(root);
|
|
@@ -128,7 +163,9 @@ function createCoreRuntimeAdapter(options = {}) {
|
|
|
128
163
|
.find((workspace) => samePath(workspace.path, workspacePath));
|
|
129
164
|
const selectedWorkspace = knownWorkspace
|
|
130
165
|
? agent.selectWorkspaceFromStorage(knownWorkspace.id)
|
|
131
|
-
:
|
|
166
|
+
: samePath(workspacePath, root)
|
|
167
|
+
? (agent.workspace.current || agent.createInternalWorkspace())
|
|
168
|
+
: agent.addExternalWorkspace(workspacePath);
|
|
132
169
|
if (!selectedWorkspace) {
|
|
133
170
|
throw new Error(
|
|
134
171
|
`The current folder cannot be registered as a Newmark workspace: ${workspacePath}. ` +
|
|
@@ -454,4 +491,4 @@ function createCoreRuntimeAdapter(options = {}) {
|
|
|
454
491
|
};
|
|
455
492
|
}
|
|
456
493
|
|
|
457
|
-
module.exports = { createCoreRuntimeAdapter, mergeProviderConfig, resolveDesktopDist, samePath, sanitizeProviders };
|
|
494
|
+
module.exports = { createCoreRuntimeAdapter, mergeProviderConfig, resolveDesktopDist, samePath, safeWorkspacePath, sanitizeProviders };
|
package/dist/tui/src/app.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const readline = require("node:readline");
|
|
4
4
|
const path = require("node:path");
|
|
5
5
|
const { render } = require("./render");
|
|
6
|
+
const { targetKey } = require("./adapters/newmark-contract");
|
|
6
7
|
const {
|
|
7
8
|
activateMenu,
|
|
8
9
|
activateMemorySelection,
|
|
@@ -89,6 +90,20 @@ function executeAction(state, action) {
|
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
function argumentValue(args, name) {
|
|
94
|
+
const inline = args.find((arg) => String(arg).startsWith(`${name}=`));
|
|
95
|
+
if (inline) return String(inline).slice(name.length + 1);
|
|
96
|
+
const index = args.indexOf(name);
|
|
97
|
+
return index >= 0 ? args[index + 1] || "" : "";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveTuiWorkspacePath(args, options = {}) {
|
|
101
|
+
const explicitWorkspace = options.workspacePath || argumentValue(args, "--workspace");
|
|
102
|
+
if (explicitWorkspace) return explicitWorkspace;
|
|
103
|
+
const explicitRoot = options.root || argumentValue(args, "--root");
|
|
104
|
+
return explicitRoot || process.cwd();
|
|
105
|
+
}
|
|
106
|
+
|
|
92
107
|
function start(options = {}) {
|
|
93
108
|
const forcedTerminal = process.env.NEWMARK_FORCE_TTY === "1";
|
|
94
109
|
if ((!process.stdin.isTTY || !process.stdout.isTTY) && !forcedTerminal) {
|
|
@@ -98,20 +113,16 @@ function start(options = {}) {
|
|
|
98
113
|
}
|
|
99
114
|
|
|
100
115
|
const args = process.argv.slice(2);
|
|
101
|
-
const optionValue = (name) => {
|
|
102
|
-
const index = args.indexOf(name);
|
|
103
|
-
return index >= 0 ? args[index + 1] : "";
|
|
104
|
-
};
|
|
105
116
|
let adapter;
|
|
106
117
|
try {
|
|
107
118
|
if (args.includes("--demo") || process.env.NEWMARK_TUI_DEMO === "1") {
|
|
108
119
|
adapter = require("./adapters/mock-newmark-adapter").createMockNewmarkAdapter();
|
|
109
120
|
} else {
|
|
110
|
-
const root = options.root ||
|
|
111
|
-
const workspacePath = options.workspacePath
|
|
121
|
+
const root = options.root || argumentValue(args, "--root");
|
|
122
|
+
const workspacePath = resolveTuiWorkspacePath(args, { root, workspacePath: options.workspacePath });
|
|
112
123
|
adapter = require("./adapters/core-runtime-adapter").createCoreRuntimeAdapter({
|
|
113
124
|
root: root ? path.resolve(root) : undefined,
|
|
114
|
-
workspacePath:
|
|
125
|
+
workspacePath: path.resolve(workspacePath),
|
|
115
126
|
desktopDist: options.desktopDist
|
|
116
127
|
});
|
|
117
128
|
}
|
|
@@ -185,6 +196,7 @@ function start(options = {}) {
|
|
|
185
196
|
|
|
186
197
|
async function sendRealMessage(text) {
|
|
187
198
|
const target = { ...state.target };
|
|
199
|
+
const key = targetKey(target);
|
|
188
200
|
state.input = "";
|
|
189
201
|
state.inputCursor = 0;
|
|
190
202
|
state.inputMode = false;
|
|
@@ -201,13 +213,26 @@ function start(options = {}) {
|
|
|
201
213
|
}, 250);
|
|
202
214
|
}
|
|
203
215
|
paint();
|
|
216
|
+
const previous = state.sendQueueByConversation?.get(key) || Promise.resolve();
|
|
217
|
+
const current = previous
|
|
218
|
+
.catch(() => {})
|
|
219
|
+
.then(() => state.adapter.sendMessage(text, target));
|
|
220
|
+
state.sendQueueByConversation?.set(key, current);
|
|
204
221
|
try {
|
|
205
|
-
const snapshot = await
|
|
206
|
-
|
|
222
|
+
const snapshot = await current;
|
|
223
|
+
if (state.sendQueueByConversation?.get(key) === current) {
|
|
224
|
+
applyConversationResult(state, target, snapshot);
|
|
225
|
+
}
|
|
207
226
|
} catch (error) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
227
|
+
if (state.sendQueueByConversation?.get(key) === current) {
|
|
228
|
+
markConversationRunning(state, target, false);
|
|
229
|
+
state.notice = `Agent error: ${error.message}`;
|
|
230
|
+
paint();
|
|
231
|
+
}
|
|
232
|
+
} finally {
|
|
233
|
+
if (state.sendQueueByConversation?.get(key) === current) {
|
|
234
|
+
state.sendQueueByConversation.delete(key);
|
|
235
|
+
}
|
|
211
236
|
}
|
|
212
237
|
}
|
|
213
238
|
|
|
@@ -439,6 +464,15 @@ function start(options = {}) {
|
|
|
439
464
|
|
|
440
465
|
function handleKey(str, key) {
|
|
441
466
|
if (key.ctrl && key.name === "c") return quit();
|
|
467
|
+
// A real run leaves input mode immediately after Enter. Keep Esc target-bound
|
|
468
|
+
// at the app boundary so users can stop that run from the content view too.
|
|
469
|
+
if (key.name === "escape"
|
|
470
|
+
&& !state.overlay
|
|
471
|
+
&& !state.memorySearchActive
|
|
472
|
+
&& state.runningConversationKeys?.size) {
|
|
473
|
+
Promise.resolve(requestConversationStop(state)).finally(paint);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
442
476
|
if (state.overlay === "palette") handlePalette(str, key);
|
|
443
477
|
else if (state.overlay === "flow-select") handleFlowSelection(key);
|
|
444
478
|
else if (state.overlay === "settings-choice") handleSettingChoice(key);
|
|
@@ -501,4 +535,4 @@ function start(options = {}) {
|
|
|
501
535
|
paint();
|
|
502
536
|
}
|
|
503
537
|
|
|
504
|
-
module.exports = { createPaintScheduler, executeAction, start };
|
|
538
|
+
module.exports = { createPaintScheduler, executeAction, resolveTuiWorkspacePath, start };
|
package/dist/tui/src/render.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const data = require("./data");
|
|
4
|
-
const {
|
|
4
|
+
const {
|
|
5
|
+
activeConversationModelLabel,
|
|
6
|
+
filteredCommands,
|
|
7
|
+
memoryTagOptions,
|
|
8
|
+
resolveThemeAppearance,
|
|
9
|
+
selectedMemoryDetail
|
|
10
|
+
} = require("./state");
|
|
5
11
|
const { SETTINGS_CATEGORIES, displaySettingValue, settingsRows } = require("./settings-schema");
|
|
6
12
|
|
|
7
13
|
const ESC = "\u001b[";
|
|
@@ -94,11 +100,16 @@ function palette(state) {
|
|
|
94
100
|
};
|
|
95
101
|
const colors = state.theme === "light" ? light : dark;
|
|
96
102
|
const appearance = state.settings?.personalization;
|
|
97
|
-
if (!appearance
|
|
103
|
+
if (!appearance) {
|
|
98
104
|
return { ...colors, paint: "", final: `${ESC}0m` };
|
|
99
105
|
}
|
|
100
|
-
const
|
|
101
|
-
|
|
106
|
+
const resolvedAppearance = resolveThemeAppearance(
|
|
107
|
+
state.theme,
|
|
108
|
+
appearance.fontColor,
|
|
109
|
+
appearance.backgroundColor
|
|
110
|
+
);
|
|
111
|
+
const [fr, fg, fb] = hexRgb(resolvedAppearance.fontColor);
|
|
112
|
+
const [br, bg, bb] = hexRgb(resolvedAppearance.backgroundColor);
|
|
102
113
|
const paint = `${ESC}38;2;${fr};${fg};${fb}m${ESC}48;2;${br};${bg};${bb}m`;
|
|
103
114
|
return {
|
|
104
115
|
...colors,
|
|
@@ -951,10 +962,15 @@ function contrastRatio(foreground, background) {
|
|
|
951
962
|
|
|
952
963
|
function personalizationPreview(state, p) {
|
|
953
964
|
const appearance = state.settings.personalization;
|
|
954
|
-
const
|
|
955
|
-
|
|
965
|
+
const resolvedAppearance = resolveThemeAppearance(
|
|
966
|
+
state.theme,
|
|
967
|
+
appearance.fontColor,
|
|
968
|
+
appearance.backgroundColor
|
|
969
|
+
);
|
|
970
|
+
const [fr, fg, fb] = hexRgb(resolvedAppearance.fontColor);
|
|
971
|
+
const [br, bg, bb] = hexRgb(resolvedAppearance.backgroundColor);
|
|
956
972
|
const sample = `${ESC}38;2;${fr};${fg};${fb}m${ESC}48;2;${br};${bg};${bb}m Newmark Aa 中 123 ${p.reset}`;
|
|
957
|
-
const contrast = contrastRatio(
|
|
973
|
+
const contrast = contrastRatio(resolvedAppearance.fontColor, resolvedAppearance.backgroundColor);
|
|
958
974
|
return [
|
|
959
975
|
`${p.bold}Live color preview${p.reset} ${sample}`,
|
|
960
976
|
`${p.muted}Font request: ${appearance.fontFamily} · contrast ${contrast.toFixed(2)}:1 ${contrast >= 4.5 ? "PASS" : "LOW"}${p.reset}`,
|
package/dist/tui/src/state.js
CHANGED
|
@@ -5,6 +5,55 @@ const { createMockNewmarkAdapter } = require("./adapters/mock-newmark-adapter");
|
|
|
5
5
|
const { targetKey, validateSnapshot } = require("./adapters/newmark-contract");
|
|
6
6
|
const { SETTINGS_CATEGORIES, settingsRows } = require("./settings-schema");
|
|
7
7
|
const INTELLIGENCE_TIERS = Object.freeze(["low", "medium", "high", "xhigh", "max", "ultra"]);
|
|
8
|
+
const HEX_COLOR_PATTERN = /^#[0-9a-f]{6}$/i;
|
|
9
|
+
const THEME_APPEARANCE_DEFAULTS = Object.freeze({
|
|
10
|
+
dark: Object.freeze({ fontColor: "#E6EAF2", backgroundColor: "#0A0A1A" }),
|
|
11
|
+
light: Object.freeze({ fontColor: "#1F2937", backgroundColor: "#F0F2F8" })
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
function normalizedTheme(value) {
|
|
15
|
+
return String(value || "dark").trim().toLowerCase() === "light" ? "light" : "dark";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizedHexColor(value) {
|
|
19
|
+
const candidate = String(value || "").trim();
|
|
20
|
+
return HEX_COLOR_PATTERN.test(candidate) ? candidate.toUpperCase() : "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function contrastRatio(foreground, background) {
|
|
24
|
+
const luminance = (value) => {
|
|
25
|
+
const channels = [0, 2, 4].map((offset) => Number.parseInt(value.slice(offset, offset + 2), 16) / 255)
|
|
26
|
+
.map((channel) => channel <= 0.03928 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4);
|
|
27
|
+
return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
|
|
28
|
+
};
|
|
29
|
+
const foregroundLuminance = luminance(foreground.slice(1));
|
|
30
|
+
const backgroundLuminance = luminance(background.slice(1));
|
|
31
|
+
return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05)
|
|
32
|
+
/ (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve persisted appearance without allowing a legacy/mismatched color
|
|
37
|
+
* pair to disable the whole terminal canvas. Explicit colors are retained
|
|
38
|
+
* when readable; otherwise the closest theme-safe pair is used for rendering.
|
|
39
|
+
*/
|
|
40
|
+
function resolveThemeAppearance(theme, fontColor, backgroundColor) {
|
|
41
|
+
const resolvedTheme = normalizedTheme(theme);
|
|
42
|
+
const defaults = THEME_APPEARANCE_DEFAULTS[resolvedTheme];
|
|
43
|
+
let foreground = normalizedHexColor(fontColor) || defaults.fontColor;
|
|
44
|
+
let background = normalizedHexColor(backgroundColor) || defaults.backgroundColor;
|
|
45
|
+
if (contrastRatio(foreground, background) < 4.5) {
|
|
46
|
+
if (contrastRatio(defaults.fontColor, background) >= 4.5) {
|
|
47
|
+
foreground = defaults.fontColor;
|
|
48
|
+
} else if (contrastRatio(foreground, defaults.backgroundColor) >= 4.5) {
|
|
49
|
+
background = defaults.backgroundColor;
|
|
50
|
+
} else {
|
|
51
|
+
foreground = defaults.fontColor;
|
|
52
|
+
background = defaults.backgroundColor;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { theme: resolvedTheme, fontColor: foreground, backgroundColor: background };
|
|
56
|
+
}
|
|
8
57
|
|
|
9
58
|
function applySnapshot(state, snapshot) {
|
|
10
59
|
const valid = validateSnapshot(snapshot);
|
|
@@ -53,15 +102,15 @@ function applyThemeAppearance(state, selection) {
|
|
|
53
102
|
: String(selection || "Dark").toLowerCase() === "system"
|
|
54
103
|
? "System"
|
|
55
104
|
: "Dark";
|
|
56
|
-
const
|
|
57
|
-
state.theme =
|
|
105
|
+
const appearance = resolveThemeAppearance(label, "", "");
|
|
106
|
+
state.theme = appearance.theme;
|
|
58
107
|
state.settings.personalization.theme = label;
|
|
59
|
-
state.settings.personalization.fontColor =
|
|
60
|
-
state.settings.personalization.backgroundColor =
|
|
108
|
+
state.settings.personalization.fontColor = appearance.fontColor;
|
|
109
|
+
state.settings.personalization.backgroundColor = appearance.backgroundColor;
|
|
61
110
|
return {
|
|
62
111
|
theme: label.toLowerCase(),
|
|
63
|
-
fontColor:
|
|
64
|
-
backgroundColor:
|
|
112
|
+
fontColor: appearance.fontColor,
|
|
113
|
+
backgroundColor: appearance.backgroundColor
|
|
65
114
|
};
|
|
66
115
|
}
|
|
67
116
|
|
|
@@ -101,6 +150,7 @@ function createState(options = {}) {
|
|
|
101
150
|
}
|
|
102
151
|
return [name, workflow || { name, components: [] }];
|
|
103
152
|
}));
|
|
153
|
+
const appearance = resolveThemeAppearance(snapshot.darkMode, snapshot.fontColor, snapshot.backgroundColor);
|
|
104
154
|
return {
|
|
105
155
|
adapter,
|
|
106
156
|
adapterKind: adapter.kind,
|
|
@@ -135,7 +185,7 @@ function createState(options = {}) {
|
|
|
135
185
|
flowSelectionIndex: 0,
|
|
136
186
|
flowByConversation: initialFlow ? { [`${target.workspaceId}::${target.conversationId}`]: initialFlow } : {},
|
|
137
187
|
currentFlow: initialFlow,
|
|
138
|
-
theme:
|
|
188
|
+
theme: appearance.theme,
|
|
139
189
|
overlay: null,
|
|
140
190
|
settingChoiceTab: "",
|
|
141
191
|
settingChoiceKey: "",
|
|
@@ -161,6 +211,7 @@ function createState(options = {}) {
|
|
|
161
211
|
: `Newmark core connected · ${workspaces.find((item) => item.id === target.workspaceId)?.path || target.workspaceId}`,
|
|
162
212
|
busy: false,
|
|
163
213
|
runningConversationKeys: new Set(),
|
|
214
|
+
sendQueueByConversation: new Map(),
|
|
164
215
|
stopStageByConversation: new Map(),
|
|
165
216
|
tick: 0,
|
|
166
217
|
messages: snapshot.chatMessages.map((item) => ({ ...item })),
|
|
@@ -199,8 +250,8 @@ function createState(options = {}) {
|
|
|
199
250
|
personalization: {
|
|
200
251
|
theme: { light: "Light", system: "System", dark: "Dark" }[String(snapshot.darkMode || "dark").toLowerCase()] || "Dark",
|
|
201
252
|
fontFamily: snapshot.fontFamily || "Terminal default",
|
|
202
|
-
fontColor:
|
|
203
|
-
backgroundColor:
|
|
253
|
+
fontColor: appearance.fontColor,
|
|
254
|
+
backgroundColor: appearance.backgroundColor,
|
|
204
255
|
glassAlpha: Number(snapshot.glassAlpha || 0.85)
|
|
205
256
|
},
|
|
206
257
|
runtime: {
|
|
@@ -1285,6 +1336,7 @@ module.exports = {
|
|
|
1285
1336
|
activateMenu,
|
|
1286
1337
|
activateMemorySelection,
|
|
1287
1338
|
applyThemeAppearance,
|
|
1339
|
+
resolveThemeAppearance,
|
|
1288
1340
|
applySnapshot,
|
|
1289
1341
|
applyConversationResult,
|
|
1290
1342
|
beginAutomationCreate,
|