termux-dev 1.9.5 → 1.9.7
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 +29 -0
- package/assets/banner.svg +1 -1
- package/assets/preview.svg +1 -1
- package/dist/cli/index.js +15 -5
- package/dist/cli/update-screen.js +6 -2
- package/dist/cli/updater.js +59 -5
- package/dist/core/attention.js +63 -0
- package/dist/core/types.js +1 -1
- package/dist/permissions/guard.js +4 -0
- package/dist/tools/questions.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,6 +67,8 @@ The terminal interface is built around a framed prompt that carries the status i
|
|
|
67
67
|
Built-in support for TypeScript (`tsc`), JavaScript (`node --check`), Python (`py_compile`), and Rust (`cargo check`). The AI automatically finds and fixes errors before finishing turns.
|
|
68
68
|
- **Project Memory Bank (`.devx/memory.md`):**
|
|
69
69
|
Persistent project context across sessions with `/memory add <rule>`.
|
|
70
|
+
- **Notifications when devx waits for you:**
|
|
71
|
+
When the agent needs an approval or asks you a question (`◆ Clarify`) and the terminal is not in front, you get a notification (kitty's own protocol, `notify-send` on Linux, `osascript` on macOS; on Termux an Android notification with a short vibration). A finished task is announced too, with the start of the answer. The terminal has to report focus changes (most modern ones do); when devx cannot tell whether you are looking, it stays quiet instead of popping up in front of you.
|
|
70
72
|
- **Past chats (`search_chats`):**
|
|
71
73
|
When you say "as we decided last time" or "in that chat about the parser", the agent can look through your saved chats by keywords (any language, other word forms match: page / pages) and read the one it needs, a few messages at a time. It is read-only and local, so it asks nothing; the chat that is open is not searched; tool output is not indexed; and what it reads is handed to the model as data, not as instructions. The agent does not use it on a normal task.
|
|
72
74
|
- **18 Colour Themes on a Pure Black Background (`#0a0a0c`):**
|
|
@@ -92,6 +94,33 @@ The terminal interface is built around a framed prompt that carries the status i
|
|
|
92
94
|
- **Node.js**: `v20.0.0` or higher
|
|
93
95
|
- **npm**, **pnpm**, or **yarn**
|
|
94
96
|
|
|
97
|
+
### Installing Node.js and npm
|
|
98
|
+
|
|
99
|
+
devx needs Node.js 20 or newer (npm comes with it). Pick your system:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Windows (PowerShell)
|
|
103
|
+
winget install OpenJS.NodeJS.LTS
|
|
104
|
+
|
|
105
|
+
# macOS (Homebrew)
|
|
106
|
+
brew install node
|
|
107
|
+
|
|
108
|
+
# Ubuntu / Debian (NodeSource: the version in the distribution's own repository is often older than 20)
|
|
109
|
+
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
|
|
110
|
+
sudo apt-get install -y nodejs
|
|
111
|
+
|
|
112
|
+
# Fedora
|
|
113
|
+
sudo dnf install -y nodejs
|
|
114
|
+
|
|
115
|
+
# Arch Linux
|
|
116
|
+
sudo pacman -S nodejs npm
|
|
117
|
+
|
|
118
|
+
# Android (Termux)
|
|
119
|
+
pkg update && pkg install -y nodejs-lts
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Reopen the terminal and check: `node -v` (should be `v20` or higher) and `npm -v`. Any system: the installer at [nodejs.org](https://nodejs.org/en/download) or [nvm](https://github.com/nvm-sh/nvm).
|
|
123
|
+
|
|
95
124
|
### Fast Global Install (Recommended)
|
|
96
125
|
|
|
97
126
|
```bash
|
package/assets/banner.svg
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<rect width="1000" height="280" fill="url(#bg)"/>
|
|
17
17
|
</g>
|
|
18
18
|
<rect x="1.5" y="1.5" width="997" height="277" rx="18" fill="none" stroke="#23252c" stroke-width="1.5"/>
|
|
19
|
-
<text x="500.0" y="70" text-anchor="middle" font-family="'Inter',system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif" font-size="13" font-weight="600" letter-spacing="5" fill="#7d8896">THE TERMINAL-NATIVE AI CODING AGENT<tspan dx="16" fill="#2fe3ee" font-weight="700">v1.9.
|
|
19
|
+
<text x="500.0" y="70" text-anchor="middle" font-family="'Inter',system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif" font-size="13" font-weight="600" letter-spacing="5" fill="#7d8896">THE TERMINAL-NATIVE AI CODING AGENT<tspan dx="16" fill="#2fe3ee" font-weight="700">v1.9.7</tspan></text>
|
|
20
20
|
<g>
|
|
21
21
|
<text x="500.0" y="178" text-anchor="middle" font-family="'JetBrains Mono','Fira Code','SF Mono',ui-monospace,Menlo,Consolas,'DejaVu Sans Mono',monospace" font-size="112" font-weight="800" letter-spacing="14" fill="#2fe3ee">DEVX</text>
|
|
22
22
|
</g>
|
package/assets/preview.svg
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
<rect x="595.2" y="574.0" width="243.2" height="19" fill="#000000"/>
|
|
33
33
|
<text x="44.8" y="62.0" class="t" fill="#b794f6">▌</text>
|
|
34
34
|
<text x="61.6" y="62.0" class="t b" fill="#f4f7fb">devx</text>
|
|
35
|
-
<text x="112.0" y="62.0" class="t" fill="#5f6b7a">v1.9.
|
|
35
|
+
<text x="112.0" y="62.0" class="t" fill="#5f6b7a">v1.9.7</text>
|
|
36
36
|
<text x="61.6" y="106.0" class="t" fill="#5f6b7a">model</text>
|
|
37
37
|
<text x="128.8" y="106.0" class="t" fill="#e5eaf1">deepseek-v4.1-flash · think high</text>
|
|
38
38
|
<text x="406.0" y="106.0" class="t" fill="#5f6b7a">/model</text>
|
package/dist/cli/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { globalSnapshotManager } from '../core/snapshot.js';
|
|
|
17
17
|
import { MemoryManager, memoryForPrompt } from '../core/memory.js';
|
|
18
18
|
import { startServer, stopServer, displayServerBanner } from './server.js';
|
|
19
19
|
import { notifyDevice, acquireWakeLock, isTermux, notificationBody, sendDesktopNotification } from '../core/notify.js';
|
|
20
|
+
import { isTerminalFocused, setFocusTracking, setTerminalFocused } from '../core/attention.js';
|
|
20
21
|
import { copyToClipboard } from './clipboard-write.js';
|
|
21
22
|
import { REWIND_CURRENT, messageDraft, rewindItems, userEntries } from './rewind.js';
|
|
22
23
|
import { runPicker } from './picker.js';
|
|
@@ -933,9 +934,14 @@ export async function main() {
|
|
|
933
934
|
const sessionManager = new SessionManager(config.model, planMode);
|
|
934
935
|
drawLogo(config.banner || 'full');
|
|
935
936
|
await runStartupUpdateCheck(config, { save: saveConfig });
|
|
936
|
-
await MCPManager.getInstance().init(undefined, config.projectMcpTrusted === true);
|
|
937
|
-
// After the servers are up: the prompt says how the agent reaches the tools it has to load.
|
|
938
937
|
history.addMessage({ role: 'system', content: await buildSystemPrompt(planMode) });
|
|
938
|
+
// The servers start while the prompt is already there (starting them takes seconds: `npx` and the
|
|
939
|
+
// network). What needs them waits for `mcpReady`: the first request, and /mcp. When they are up the
|
|
940
|
+
// prompt is written again, so that it says how the agent reaches the tools that wait to be loaded.
|
|
941
|
+
const mcpReady = MCPManager.getInstance()
|
|
942
|
+
.init(undefined, config.projectMcpTrusted === true)
|
|
943
|
+
.then(async () => { history.updateSystemPrompt(await buildSystemPrompt(planMode)); })
|
|
944
|
+
.catch(() => undefined);
|
|
939
945
|
// Ask a custom endpoint what the current model can do (cached for a day; never blocks the prompt).
|
|
940
946
|
void refreshEndpoint(config).catch(() => null);
|
|
941
947
|
const sessionCost = new CostLedger();
|
|
@@ -988,7 +994,6 @@ export async function main() {
|
|
|
988
994
|
// The name of the chat: in the title of the terminal window (`◈ Short title`) and in the list of sessions.
|
|
989
995
|
let windowTitle = '';
|
|
990
996
|
let titleAttempts = 0;
|
|
991
|
-
let terminalFocused = true; // updated by the terminal's focus reports while a task runs
|
|
992
997
|
const setWindowTitle = (title) => {
|
|
993
998
|
windowTitle = title;
|
|
994
999
|
if (process.stdout.isTTY)
|
|
@@ -1858,6 +1863,7 @@ export async function main() {
|
|
|
1858
1863
|
continue;
|
|
1859
1864
|
}
|
|
1860
1865
|
if (cmd === '/mcp') {
|
|
1866
|
+
await mcpReady;
|
|
1861
1867
|
const sub = (parts[1] || '').toLowerCase();
|
|
1862
1868
|
if (sub === 'reload' || sub === 'restart' || sub === 'r') {
|
|
1863
1869
|
const s = new UiSpinner();
|
|
@@ -2109,6 +2115,8 @@ export async function main() {
|
|
|
2109
2115
|
});
|
|
2110
2116
|
// The format of the gateway is learned once (cached); the first request waits for it.
|
|
2111
2117
|
await refreshFormats(config).catch(() => null);
|
|
2118
|
+
// The MCP servers may still be starting (see mcpReady): the request goes out with their tools.
|
|
2119
|
+
await mcpReady;
|
|
2112
2120
|
const provider = createProvider(config);
|
|
2113
2121
|
const tools = getToolSource(planMode);
|
|
2114
2122
|
const guard = new CLIConsoleGuard(config.autoApprove, [...(config.bashAllowlist || [])], {
|
|
@@ -2424,7 +2432,7 @@ export async function main() {
|
|
|
2424
2432
|
let text = (Buffer.isBuffer(data) ? data : Buffer.from(String(data))).toString('utf8');
|
|
2425
2433
|
// Focus reports: ESC [ I (got the focus) and ESC [ O (lost it). They are not keys.
|
|
2426
2434
|
if (text.includes('\x1b[I') || text.includes('\x1b[O')) {
|
|
2427
|
-
|
|
2435
|
+
setTerminalFocused(text.lastIndexOf('\x1b[I') > text.lastIndexOf('\x1b[O'));
|
|
2428
2436
|
text = text.replace(/\x1b\[[IO]/g, '');
|
|
2429
2437
|
if (!text)
|
|
2430
2438
|
return;
|
|
@@ -2468,6 +2476,7 @@ export async function main() {
|
|
|
2468
2476
|
process.stdout.on('resize', onResize);
|
|
2469
2477
|
uiSpinner?.mute(true);
|
|
2470
2478
|
process.stdout.write('\x1b[?1004h'); // the terminal tells when it loses or gets the focus
|
|
2479
|
+
setFocusTracking(true); // approval cards and questions notify when it is not in front
|
|
2471
2480
|
}
|
|
2472
2481
|
}
|
|
2473
2482
|
catch { }
|
|
@@ -2843,7 +2852,7 @@ export async function main() {
|
|
|
2843
2852
|
if (shouldNotify)
|
|
2844
2853
|
notifyDevice(heading, preview || `Task completed in ${secs}s!`);
|
|
2845
2854
|
}
|
|
2846
|
-
else if (!
|
|
2855
|
+
else if (!isTerminalFocused()) {
|
|
2847
2856
|
// The terminal is not in front: a notification with the start of the answer.
|
|
2848
2857
|
if (sendDesktopNotification(heading, preview || `Task completed in ${secs}s`) === 'none' && shouldNotify)
|
|
2849
2858
|
notifyDevice(heading, preview || `Task completed in ${secs}s!`);
|
|
@@ -2932,6 +2941,7 @@ export async function main() {
|
|
|
2932
2941
|
const unsent = composer.stop();
|
|
2933
2942
|
if (process.stdout.isTTY)
|
|
2934
2943
|
process.stdout.write('\x1b[?1004l');
|
|
2944
|
+
setFocusTracking(false);
|
|
2935
2945
|
releasePauseHook();
|
|
2936
2946
|
process.stdout.removeListener('resize', onResize);
|
|
2937
2947
|
if (!currentDraft && (unsent.draft || unsent.queued.length > 0)) {
|
|
@@ -4,7 +4,7 @@ import { runPicker } from './picker.js';
|
|
|
4
4
|
import { palette, renderNotice } from './ui.js';
|
|
5
5
|
import { displayWidth, truncateWidth } from './width.js';
|
|
6
6
|
import { getLanguage, t } from '../core/i18n.js';
|
|
7
|
-
import { UpdateError, buildUpdateSteps, checkForUpdates, detectInstallMethod, getCurrentVersion, restartDevx, runUpdateSteps, } from './updater.js';
|
|
7
|
+
import { UpdateError, buildUpdateSteps, checkForUpdates, checkForUpdatesRemembered, detectInstallMethod, readCachedCheck, getCurrentVersion, restartDevx, runUpdateSteps, } from './updater.js';
|
|
8
8
|
/**
|
|
9
9
|
* The update screens, in the style of the rest of devx:
|
|
10
10
|
*
|
|
@@ -344,7 +344,9 @@ export async function runStartupUpdateCheck(config, deps = {}) {
|
|
|
344
344
|
if (!out.isTTY)
|
|
345
345
|
return;
|
|
346
346
|
const current = await getCurrentVersion();
|
|
347
|
-
|
|
347
|
+
// A recent answer is used as it is: no network, no "checking" line, no wait before the prompt.
|
|
348
|
+
const cached = deps.check ? undefined : await readCachedCheck();
|
|
349
|
+
const result = cached ?? await withCheckingLine(out, current, () => (deps.check ?? checkForUpdatesRemembered)(6000));
|
|
348
350
|
if (result.updateAvailable) {
|
|
349
351
|
if (config.skippedUpdate === result.latestVersion)
|
|
350
352
|
return;
|
|
@@ -359,6 +361,8 @@ export async function runStartupUpdateCheck(config, deps = {}) {
|
|
|
359
361
|
}
|
|
360
362
|
if (result.error)
|
|
361
363
|
return; // offline at start-up is not worth a word
|
|
364
|
+
if (cached)
|
|
365
|
+
return; // up to date, as the last check said: nothing to show
|
|
362
366
|
// Up to date: a short line that goes away by itself.
|
|
363
367
|
out.write(` ${palette.ok('✓')} ${palette.muted(t('update.upToDate', { version: result.currentVersion }))}\n`);
|
|
364
368
|
await sleep(deps.tickMs !== undefined ? 0 : 450);
|
package/dist/cli/updater.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
3
4
|
import fs from 'fs/promises';
|
|
4
5
|
import { fileURLToPath } from 'url';
|
|
5
6
|
const GITHUB_REPO_URL = 'https://github.com/apvcode/Termux-Dev';
|
|
@@ -40,6 +41,12 @@ export async function checkForUpdates(timeoutMs = 10000) {
|
|
|
40
41
|
// mobile connection a stalled second request used to block devx at startup forever.
|
|
41
42
|
const signal = AbortSignal.timeout(timeoutMs);
|
|
42
43
|
try {
|
|
44
|
+
// The release is asked at the same time as the package.json, not after it: the check takes as long
|
|
45
|
+
// as the slower of the two, not both. A failure of the release request is not an error of the check.
|
|
46
|
+
const releaseRequest = fetch(`${REMOTE_RELEASE_URL}?t=${Date.now()}`, {
|
|
47
|
+
signal,
|
|
48
|
+
headers: { 'User-Agent': 'devx-updater' }
|
|
49
|
+
}).catch(() => null);
|
|
43
50
|
// 1. Try fetching latest package.json with cache-busting timestamp
|
|
44
51
|
const cacheBusterUrl = `${REMOTE_PKG_URL}?t=${Date.now()}`;
|
|
45
52
|
const res = await fetch(cacheBusterUrl, {
|
|
@@ -59,11 +66,8 @@ export async function checkForUpdates(timeoutMs = 10000) {
|
|
|
59
66
|
}
|
|
60
67
|
// 2. Also check latest GitHub release tag
|
|
61
68
|
try {
|
|
62
|
-
const relRes = await
|
|
63
|
-
|
|
64
|
-
headers: { 'User-Agent': 'devx-updater' }
|
|
65
|
-
});
|
|
66
|
-
if (relRes.ok) {
|
|
69
|
+
const relRes = await releaseRequest;
|
|
70
|
+
if (relRes?.ok) {
|
|
67
71
|
const relData = await relRes.json();
|
|
68
72
|
const relTag = (relData.tag_name || relData.name || '').replace(/^v/, '').trim();
|
|
69
73
|
if (relTag && (!latestVersion || isNewerVersion(relTag, latestVersion))) {
|
|
@@ -99,6 +103,56 @@ export async function checkForUpdates(timeoutMs = 10000) {
|
|
|
99
103
|
};
|
|
100
104
|
}
|
|
101
105
|
}
|
|
106
|
+
// ───────────────────────── remembering the check ─────────────────────────
|
|
107
|
+
/** An answer is good for this long; after it devx asks again. */
|
|
108
|
+
const CHECK_FRESH_MS = 6 * 60 * 60 * 1000;
|
|
109
|
+
/** A failed check (offline, a stalled connection) is not repeated at every start: that would stall every start. */
|
|
110
|
+
const CHECK_FAILED_MS = 30 * 60 * 1000;
|
|
111
|
+
const checkFile = () => path.join(os.homedir(), '.devx', 'update-check.json');
|
|
112
|
+
/** The last check, when it is recent enough to be trusted at start-up; undefined when devx has to ask again. */
|
|
113
|
+
export async function readCachedCheck(now = Date.now()) {
|
|
114
|
+
try {
|
|
115
|
+
const saved = JSON.parse(await fs.readFile(checkFile(), 'utf8'));
|
|
116
|
+
const age = now - Number(saved.checkedAt);
|
|
117
|
+
// A clock that went backwards makes the age negative: such an answer is not trusted.
|
|
118
|
+
if (!Number.isFinite(age) || age < 0)
|
|
119
|
+
return undefined;
|
|
120
|
+
const currentVersion = await getCurrentVersion();
|
|
121
|
+
if (saved.error) {
|
|
122
|
+
return age < CHECK_FAILED_MS ? { updateAvailable: false, currentVersion, latestVersion: currentVersion, error: saved.error } : undefined;
|
|
123
|
+
}
|
|
124
|
+
if (age >= CHECK_FRESH_MS || !saved.latestVersion)
|
|
125
|
+
return undefined;
|
|
126
|
+
// Judged against the version that runs now: after an update the old answer no longer says "update".
|
|
127
|
+
return {
|
|
128
|
+
updateAvailable: isNewerVersion(saved.latestVersion, currentVersion),
|
|
129
|
+
currentVersion,
|
|
130
|
+
latestVersion: saved.latestVersion,
|
|
131
|
+
releaseNotes: saved.releaseNotes || undefined,
|
|
132
|
+
publishedAt: saved.publishedAt || undefined,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Keeps the answer of a check for the next start-ups. Never throws: this is only a shortcut. */
|
|
140
|
+
export async function rememberCheck(result, now = Date.now()) {
|
|
141
|
+
try {
|
|
142
|
+
const saved = result.error
|
|
143
|
+
? { checkedAt: now, error: result.error }
|
|
144
|
+
: { checkedAt: now, latestVersion: result.latestVersion, releaseNotes: result.releaseNotes, publishedAt: result.publishedAt };
|
|
145
|
+
await fs.mkdir(path.dirname(checkFile()), { recursive: true, mode: 0o700 });
|
|
146
|
+
await fs.writeFile(checkFile(), JSON.stringify(saved));
|
|
147
|
+
}
|
|
148
|
+
catch { }
|
|
149
|
+
}
|
|
150
|
+
/** `checkForUpdates`, and the answer is kept for the next start-ups (the start-up check reads it, see readCachedCheck). */
|
|
151
|
+
export async function checkForUpdatesRemembered(timeoutMs = 10000) {
|
|
152
|
+
const result = await checkForUpdates(timeoutMs);
|
|
153
|
+
await rememberCheck(result);
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
102
156
|
/** How this devx was installed: from the npm package, or from a git clone (a developer's copy). */
|
|
103
157
|
export async function detectInstallMethod() {
|
|
104
158
|
const here = fileURLToPath(import.meta.url);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { isTermux, notificationBody, notifyDevice, sendDesktopNotification } from './notify.js';
|
|
2
|
+
/**
|
|
3
|
+
* "devx is waiting for you": a notification when the agent needs an answer (an approval card, a
|
|
4
|
+
* question) and the person is not looking at the terminal. A finished task is announced elsewhere
|
|
5
|
+
* (the end of a turn); this is for the moments when the work is stopped until someone answers.
|
|
6
|
+
*/
|
|
7
|
+
let tracking = false;
|
|
8
|
+
let focused = true;
|
|
9
|
+
/**
|
|
10
|
+
* The terminal reports when it loses and gets the focus (while a task runs). Without those reports
|
|
11
|
+
* devx cannot tell whether anybody is looking, and a notification that pops up in front of someone who is
|
|
12
|
+
* typing in the terminal is worse than none, so nothing is sent then.
|
|
13
|
+
*/
|
|
14
|
+
export function setFocusTracking(on) {
|
|
15
|
+
tracking = on;
|
|
16
|
+
// A task starts because the person just pressed Enter here.
|
|
17
|
+
if (on)
|
|
18
|
+
focused = true;
|
|
19
|
+
}
|
|
20
|
+
export function setTerminalFocused(value) {
|
|
21
|
+
focused = value;
|
|
22
|
+
}
|
|
23
|
+
export function isTerminalFocused() {
|
|
24
|
+
return focused;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Tells the person that devx waits for them. Returns whether something was sent. Never throws.
|
|
28
|
+
* On Termux (Android) a notification always goes out: the person is most likely in another app, and
|
|
29
|
+
* the phone cannot tell devx. On a desktop it goes out only when the terminal is not in front.
|
|
30
|
+
*/
|
|
31
|
+
export function notifyAttention(title, body, deps = {}) {
|
|
32
|
+
try {
|
|
33
|
+
const text = notificationBody(body, 140) || title;
|
|
34
|
+
if (deps.termux ?? isTermux()) {
|
|
35
|
+
(deps.device ?? notifyDevice)(title, text);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (!tracking || focused)
|
|
39
|
+
return false;
|
|
40
|
+
const via = (deps.desktop ?? sendDesktopNotification)(title, text);
|
|
41
|
+
// No desktop notification service: the terminal bell at least marks the window as asking for attention.
|
|
42
|
+
if (via === 'none')
|
|
43
|
+
(deps.device ?? notifyDevice)(title, text);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** What an approval card asks, in a line for a notification: `bash: npm test`. */
|
|
51
|
+
export function approvalNotice(title, lines, danger) {
|
|
52
|
+
const first = (lines.find((l) => l.trim()) ?? '').trim();
|
|
53
|
+
return {
|
|
54
|
+
title: danger ? 'devx · dangerous action, approval needed' : 'devx · approval needed',
|
|
55
|
+
body: first ? `${title}: ${first}` : title,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** The question the agent asks, for a notification (and how many more follow). */
|
|
59
|
+
export function questionNotice(questions) {
|
|
60
|
+
const first = String(questions[0]?.question ?? '').trim();
|
|
61
|
+
const more = questions.length > 1 ? ` (+${questions.length - 1} more)` : '';
|
|
62
|
+
return { title: 'devx · question for you', body: `${first || 'The agent has a question'}${more}` };
|
|
63
|
+
}
|
package/dist/core/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const DEVX_VERSION = '1.9.
|
|
1
|
+
export const DEVX_VERSION = '1.9.7';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { approvalNotice, notifyAttention } from '../core/attention.js';
|
|
1
2
|
import pc from 'picocolors';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import os from 'os';
|
|
@@ -377,6 +378,9 @@ export class CLIConsoleGuard {
|
|
|
377
378
|
return false;
|
|
378
379
|
}
|
|
379
380
|
const request = buildPermissionRequest(toolName, args);
|
|
381
|
+
// The work stops here until somebody answers: if the terminal is not in front, a notification says so.
|
|
382
|
+
const notice = approvalNotice(request.title, request.lines, request.danger);
|
|
383
|
+
notifyAttention(notice.title, notice.body);
|
|
380
384
|
// Only requests that are safe to remember offer "Session" and "Always".
|
|
381
385
|
const remember = request.danger ? null : rememberKeyFor(toolName, args);
|
|
382
386
|
const offer = remember ? { session: true, always: true } : {};
|
package/dist/tools/questions.js
CHANGED
|
@@ -3,6 +3,7 @@ import { runPicker } from '../cli/picker.js';
|
|
|
3
3
|
import { palette } from '../cli/ui.js';
|
|
4
4
|
import { wrapWords } from '../cli/width.js';
|
|
5
5
|
import { t } from '../core/i18n.js';
|
|
6
|
+
import { notifyAttention, questionNotice } from '../core/attention.js';
|
|
6
7
|
export const askQuestionsTool = {
|
|
7
8
|
name: 'ask_questions',
|
|
8
9
|
definition: {
|
|
@@ -45,6 +46,9 @@ export const askQuestionsTool = {
|
|
|
45
46
|
if (!args.questions || !Array.isArray(args.questions) || args.questions.length === 0) {
|
|
46
47
|
return 'No questions provided.';
|
|
47
48
|
}
|
|
49
|
+
// The agent waits for the answer: if the terminal is not in front, a notification says so.
|
|
50
|
+
const notice = questionNotice(args.questions);
|
|
51
|
+
notifyAttention(notice.title, notice.body);
|
|
48
52
|
const results = [];
|
|
49
53
|
const total = args.questions.length;
|
|
50
54
|
const cols = process.stdout.columns || 80;
|
package/package.json
CHANGED