mixdog 0.9.0 → 0.9.1
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 +3 -3
- package/scripts/session-ingest-smoke.mjs +2 -2
- package/src/headless-role.mjs +1 -1
- package/src/lib/mixdog-debug.cjs +0 -22
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +2 -2
- package/src/mixdog-session-runtime.mjs +0 -1
- package/src/repl.mjs +0 -2
- package/src/runtime/agent/orchestrator/internal-roles.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +27 -49
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +5 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +8 -27
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +52 -182
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +8 -30
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +258 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +0 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
- package/src/runtime/channels/index.mjs +0 -30
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +0 -1
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
- package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
- package/src/runtime/channels/lib/session-discovery.mjs +0 -4
- package/src/runtime/channels/lib/tool-format.mjs +0 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +1 -10
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +3 -24
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
- package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
- package/src/runtime/memory/lib/session-ingest.mjs +9 -7
- package/src/runtime/search/index.mjs +2 -7
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +0 -1
- package/src/runtime/shared/child-spawn-gate.mjs +0 -6
- package/src/standalone/seeds.mjs +1 -11
- package/src/tui/App.jsx +35 -12
- package/src/tui/components/PromptInput.jsx +63 -2
- package/src/tui/components/ToolExecution.jsx +7 -2
- package/src/tui/components/tool-output-format.mjs +156 -22
- package/src/tui/components/tool-output-format.test.mjs +93 -1
- package/src/tui/dist/index.mjs +473 -116
- package/src/tui/markdown/format-token.mjs +267 -108
- package/src/tui/markdown/format-token.test.mjs +105 -9
- package/src/tui/theme.mjs +10 -0
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
- package/vendor/ink/build/ink.js +54 -8
- package/src/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"generated_at": "2026-06-
|
|
3
|
+
"generated_at": "2026-06-30T06:23:20.385Z",
|
|
4
4
|
"release_tag": "runtime-v0.4.0",
|
|
5
5
|
"pg": {
|
|
6
6
|
"major": 16,
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
"sha256": "8e39bfc3f06d88e9854946d5d83f7d77d9530f99284d7c1c51872c12f389f7e7",
|
|
21
21
|
"size": 23687666
|
|
22
22
|
},
|
|
23
|
+
"linux-arm64": {
|
|
24
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/runtime-v0.4.0/mixdog-runtime-linux-arm64-pg16.4-pgvector0.8.2.tar.gz",
|
|
25
|
+
"sha256": "64ff170013b9543ad8843c98cbfe14019e6be6ea3d142ec189b2bea0dd1dc797",
|
|
26
|
+
"size": 22885036
|
|
27
|
+
},
|
|
23
28
|
"linux-x64": {
|
|
24
29
|
"url": "https://github.com/tribgames/mixdog/releases/download/runtime-v0.4.0/mixdog-runtime-linux-x64-pg16.4-pgvector0.8.2.tar.gz",
|
|
25
30
|
"sha256": "28ee64125a0125ff36c47560c08ce8f38e49aef034c010c7e4867e8cdd783385",
|
|
@@ -29,12 +34,6 @@
|
|
|
29
34
|
"url": "https://github.com/tribgames/mixdog/releases/download/runtime-v0.4.0/mixdog-runtime-win32-x64-pg16.4-pgvector0.8.2.tar.gz",
|
|
30
35
|
"sha256": "6faed8a49b3303b0adb1c5715b2b19c5cc47df2e738d586d0d0bd432f1ca035d",
|
|
31
36
|
"size": 44036958
|
|
32
|
-
},
|
|
33
|
-
"linux-arm64": {
|
|
34
|
-
"unsupported": true,
|
|
35
|
-
"url": "TBD",
|
|
36
|
-
"sha256": "TBD",
|
|
37
|
-
"size": 0
|
|
38
37
|
}
|
|
39
38
|
}
|
|
40
39
|
}
|
|
@@ -788,10 +788,9 @@ async function ingestSessionMessages(args = {}) {
|
|
|
788
788
|
const m = messages[i]
|
|
789
789
|
if (!m || typeof m !== 'object') continue
|
|
790
790
|
const role = normalizeIngestRole(m.role)
|
|
791
|
-
//
|
|
792
|
-
//
|
|
793
|
-
//
|
|
794
|
-
// recent tool/system/developer state that recall fast-track must surface.
|
|
791
|
+
// ingest_session persists user/assistant only; system/developer/tool rows
|
|
792
|
+
// are dropped so recall-fasttrack summaries stay conversation-focused and
|
|
793
|
+
// do not re-inject content already in the protected system prefix.
|
|
795
794
|
if (!role) continue
|
|
796
795
|
const content = cleanMemoryText(sessionMessageContent(m))
|
|
797
796
|
if (!content || !content.trim()) continue
|
|
@@ -3666,26 +3665,6 @@ const httpServer = http.createServer(async (req, res) => {
|
|
|
3666
3665
|
return
|
|
3667
3666
|
}
|
|
3668
3667
|
|
|
3669
|
-
if (req.method === 'POST' && req.url === '/session-start/recap') {
|
|
3670
|
-
try {
|
|
3671
|
-
const body = await readBody(req)
|
|
3672
|
-
const projectId = resolveProjectScope(typeof body.cwd === 'string' && body.cwd ? body.cwd : null)
|
|
3673
|
-
const rows = projectId !== null
|
|
3674
|
-
? (await db.query(`
|
|
3675
|
-
SELECT id, ts, summary FROM entries
|
|
3676
|
-
WHERE is_root = 1 AND (project_id IS NULL OR project_id = $1)
|
|
3677
|
-
ORDER BY ts DESC, id DESC LIMIT 20
|
|
3678
|
-
`, [projectId])).rows
|
|
3679
|
-
: (await db.query(`
|
|
3680
|
-
SELECT id, ts, summary FROM entries
|
|
3681
|
-
WHERE is_root = 1
|
|
3682
|
-
ORDER BY ts DESC, id DESC LIMIT 20
|
|
3683
|
-
`)).rows
|
|
3684
|
-
sendJson(res, { ok: true, projectId, rows })
|
|
3685
|
-
} catch (e) { sendError(res, e.message) }
|
|
3686
|
-
return
|
|
3687
|
-
}
|
|
3688
|
-
|
|
3689
3668
|
if (req.method === 'POST' && req.url === '/api/tool') {
|
|
3690
3669
|
if (!isLocalOrigin(req)) {
|
|
3691
3670
|
sendJson(res, { content: [{ type: 'text', text: 'forbidden: cross-origin' }], isError: true }, 403)
|
|
@@ -48,6 +48,15 @@ function platformKey() {
|
|
|
48
48
|
return `${os}-${process.arch}`
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function platformKeyCandidates() {
|
|
52
|
+
const primary = platformKey()
|
|
53
|
+
const candidates = [primary]
|
|
54
|
+
if (process.platform === 'win32' && process.arch === 'arm64') {
|
|
55
|
+
candidates.push('win32-x64')
|
|
56
|
+
}
|
|
57
|
+
return candidates
|
|
58
|
+
}
|
|
59
|
+
|
|
51
60
|
// Fail-closed asset validation. A selected manifest asset is usable only if it
|
|
52
61
|
// is not explicitly marked unsupported AND carries a real downloadable payload:
|
|
53
62
|
// non-empty url, a well-formed 64-hex sha256, and a positive integer size.
|
|
@@ -365,25 +374,35 @@ export async function ensureRuntime(dataDir) {
|
|
|
365
374
|
gcRuntimeDir(runtimeBaseDir, readActiveVersion(runtimeBaseDir))
|
|
366
375
|
|
|
367
376
|
const manifest = await loadManifest(key)
|
|
368
|
-
const pkey
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
.join(', ') || '(none)'
|
|
379
|
-
throw new Error(
|
|
380
|
-
`[runtime-fetcher] memory runtime not available on ${pkey}: ` +
|
|
381
|
-
`no runtime asset for this platform/arch in the manifest. ` +
|
|
382
|
-
`Supported: ${supported}. ` +
|
|
383
|
-
`Memory is disabled on this platform; the rest of mixdog continues to work.`
|
|
384
|
-
)
|
|
377
|
+
const pkey = platformKey()
|
|
378
|
+
let selectedKey = null
|
|
379
|
+
let asset = null
|
|
380
|
+
for (const candidateKey of platformKeyCandidates()) {
|
|
381
|
+
const candidateAsset = manifest.assets?.[candidateKey]
|
|
382
|
+
if (isUsableAsset(candidateAsset)) {
|
|
383
|
+
selectedKey = candidateKey
|
|
384
|
+
asset = candidateAsset
|
|
385
|
+
break
|
|
386
|
+
}
|
|
385
387
|
}
|
|
386
|
-
if (!
|
|
388
|
+
if (!asset) {
|
|
389
|
+
const primaryAsset = manifest.assets?.[pkey]
|
|
390
|
+
if (!primaryAsset) {
|
|
391
|
+
// Platform/arch absent from the manifest entirely (e.g. an exotic arch).
|
|
392
|
+
// The memory PG runtime cannot start here; fail with a single clear,
|
|
393
|
+
// actionable message. The memory worker's init().catch reports this as
|
|
394
|
+
// degraded and the rest of mixdog (agent, tools) keeps working without
|
|
395
|
+
// memory.
|
|
396
|
+
const supported = Object.keys(manifest.assets || {})
|
|
397
|
+
.filter((k) => isUsableAsset(manifest.assets[k]))
|
|
398
|
+
.join(', ') || '(none)'
|
|
399
|
+
throw new Error(
|
|
400
|
+
`[runtime-fetcher] memory runtime not available on ${pkey}: ` +
|
|
401
|
+
`no runtime asset for this platform/arch in the manifest. ` +
|
|
402
|
+
`Supported: ${supported}. ` +
|
|
403
|
+
`Memory is disabled on this platform; the rest of mixdog continues to work.`
|
|
404
|
+
)
|
|
405
|
+
}
|
|
387
406
|
// Platform/arch present but explicitly marked unsupported or carrying a
|
|
388
407
|
// placeholder/TBD payload (e.g. linux-arm64). Same graceful-degrade path.
|
|
389
408
|
throw new Error(
|
|
@@ -393,6 +412,12 @@ export async function ensureRuntime(dataDir) {
|
|
|
393
412
|
)
|
|
394
413
|
}
|
|
395
414
|
|
|
415
|
+
if (selectedKey !== pkey) {
|
|
416
|
+
__mixdogMemoryLog(
|
|
417
|
+
'[runtime-fetcher] win32-arm64 has no native runtime; using win32-x64 under emulation\n'
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
|
|
396
421
|
const { url, sha256, size } = asset
|
|
397
422
|
const version = `pg${manifest.pg?.major}.${manifest.pg?.minor}+pgvector-${manifest.pgvector?.version}`
|
|
398
423
|
|
|
@@ -5,17 +5,19 @@ import crypto from 'node:crypto'
|
|
|
5
5
|
// redaction, role/content shaping) can be unit-tested without importing the
|
|
6
6
|
// MCP server entrypoint and its heavy boot-time side effects.
|
|
7
7
|
|
|
8
|
-
// Roles we persist from an in-memory session transcript
|
|
9
|
-
//
|
|
10
|
-
|
|
8
|
+
// Roles we persist from an in-memory session transcript (conversation only).
|
|
9
|
+
// Map provider/runtime spellings onto canonical roles; only user/assistant are
|
|
10
|
+
// kept so recall-fasttrack memory does not duplicate protected system prefix.
|
|
11
|
+
const INGEST_SESSION_ROLES = new Set(['user', 'assistant'])
|
|
11
12
|
|
|
12
13
|
export function normalizeIngestRole(role) {
|
|
13
14
|
const raw = String(role || '').trim().toLowerCase()
|
|
14
15
|
if (!raw) return null
|
|
15
|
-
|
|
16
|
-
if (raw === '
|
|
17
|
-
if (raw === '
|
|
18
|
-
|
|
16
|
+
let canonical = raw
|
|
17
|
+
if (raw === 'tool_result' || raw === 'function' || raw === 'tool-result') canonical = 'tool'
|
|
18
|
+
else if (raw === 'human') canonical = 'user'
|
|
19
|
+
else if (raw === 'ai' || raw === 'model') canonical = 'assistant'
|
|
20
|
+
return INGEST_SESSION_ROLES.has(canonical) ? canonical : null
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
// Extract the first textual content block from a message content field.
|
|
@@ -4,7 +4,6 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
|
5
5
|
import { z } from 'zod'
|
|
6
6
|
import fs from 'fs'
|
|
7
|
-
import path from 'path'
|
|
8
7
|
import {
|
|
9
8
|
ensureDataDir,
|
|
10
9
|
getRequestTimeoutMs,
|
|
@@ -32,7 +31,7 @@ import {
|
|
|
32
31
|
loadUsageState,
|
|
33
32
|
updateProviderState,
|
|
34
33
|
} from './lib/state.mjs'
|
|
35
|
-
import {
|
|
34
|
+
import { getScrapeCapabilities, scrapeUrls } from './lib/web-tools.mjs'
|
|
36
35
|
import { formatResponse } from './lib/formatter.mjs'
|
|
37
36
|
ensureDataDir()
|
|
38
37
|
|
|
@@ -105,10 +104,6 @@ function formattedText(tool, payload) {
|
|
|
105
104
|
}
|
|
106
105
|
}
|
|
107
106
|
|
|
108
|
-
function okText(text) {
|
|
109
|
-
return { content: [{ type: 'text', text }], isError: false }
|
|
110
|
-
}
|
|
111
|
-
|
|
112
107
|
function isInvalidSearchResult(result) {
|
|
113
108
|
const title = String(result?.title || '').trim()
|
|
114
109
|
return /\bpage not found\b|\b404\b.*\bnot found\b/i.test(title)
|
|
@@ -529,7 +524,7 @@ async function handleToolCall(name, rawArgs, options = {}) {
|
|
|
529
524
|
throw e
|
|
530
525
|
}
|
|
531
526
|
try {
|
|
532
|
-
const result = await _fetchCore(urlArgs, {
|
|
527
|
+
const result = await _fetchCore(urlArgs, { usageState, cacheState, timeoutMs, signal })
|
|
533
528
|
flushCacheState()
|
|
534
529
|
flushUsageState()
|
|
535
530
|
return {
|
|
@@ -44,10 +44,6 @@ export function writeJson(filePath, value) {
|
|
|
44
44
|
writeJsonAtomicSync(filePath, value, { lock: true, fsyncDir: true })
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export function saveConfig(config) {
|
|
48
|
-
return { ...DEFAULT_CONFIG, ...(config || {}) }
|
|
49
|
-
}
|
|
50
|
-
|
|
51
47
|
export function loadConfig() {
|
|
52
48
|
ensureDataDir()
|
|
53
49
|
if (Object.keys(readSection('search') || {}).length > 0) {
|
|
@@ -88,10 +88,6 @@ export function loadUsageState() {
|
|
|
88
88
|
return state
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
export function saveUsageState(state) {
|
|
92
|
-
scheduleUsageFlush(state)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
91
|
export function updateProviderState(state, provider, patch) {
|
|
96
92
|
let normalizedPatch = { ...patch }
|
|
97
93
|
const remaining =
|
|
@@ -125,7 +121,7 @@ export function noteProviderSuccess(state, provider, extra = {}) {
|
|
|
125
121
|
})
|
|
126
122
|
}
|
|
127
123
|
|
|
128
|
-
|
|
124
|
+
const PROVIDER_ERROR_KIND = {
|
|
129
125
|
AUTH: 'auth',
|
|
130
126
|
QUOTA: 'quota',
|
|
131
127
|
PAYMENT: 'payment',
|
|
@@ -151,16 +147,6 @@ export function classifyProviderError(error) {
|
|
|
151
147
|
return PROVIDER_ERROR_KIND.UNKNOWN
|
|
152
148
|
}
|
|
153
149
|
|
|
154
|
-
/** Structured HTTP error for search backends (enables cooldown via classifyProviderError). */
|
|
155
|
-
export function providerHttpError(provider, status, detail = '') {
|
|
156
|
-
const code = Number(status)
|
|
157
|
-
const snippet = detail ? `: ${String(detail).slice(0, 200)}` : ''
|
|
158
|
-
const err = new Error(`[search:${provider}] HTTP ${code}${snippet}`)
|
|
159
|
-
err.status = code
|
|
160
|
-
err.provider = provider
|
|
161
|
-
return err
|
|
162
|
-
}
|
|
163
|
-
|
|
164
150
|
const PROVIDER_DISABLE_TTL_MS = {
|
|
165
151
|
auth: 24 * 3600 * 1000,
|
|
166
152
|
quota: 24 * 3600 * 1000,
|
|
@@ -24,7 +24,6 @@ async function loadPuppeteer() {
|
|
|
24
24
|
return _puppeteer
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
28
27
|
const PKG_VERSION = (() => { try { return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version } catch { return '0.0.1' } })()
|
|
29
28
|
import {
|
|
30
29
|
noteProviderFailure,
|
|
@@ -137,9 +137,3 @@ export async function withGate(fn, signal = null) {
|
|
|
137
137
|
release();
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
-
|
|
141
|
-
export function gateStats() {
|
|
142
|
-
return { inflight: _inflight, queued: _queue.length, maxInflight: MAX_INFLIGHT };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export const CHILD_SPAWN_MAX_INFLIGHT = MAX_INFLIGHT;
|
package/src/standalone/seeds.mjs
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
|
|
4
|
-
const DEFAULT_DATA_FILES = [];
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
5
2
|
|
|
6
3
|
export function ensureStandaloneEnvironment({ rootDir, dataDir }) {
|
|
7
4
|
if (!rootDir) throw new Error('standalone rootDir is required');
|
|
@@ -17,11 +14,4 @@ export function ensureStandaloneEnvironment({ rootDir, dataDir }) {
|
|
|
17
14
|
process.env.MIXDOG_PATCH_NATIVE_PREWARM ??= '0';
|
|
18
15
|
|
|
19
16
|
mkdirSync(dataDir, { recursive: true });
|
|
20
|
-
for (const file of DEFAULT_DATA_FILES) {
|
|
21
|
-
const from = join(rootDir, 'defaults', file);
|
|
22
|
-
const to = join(dataDir, file);
|
|
23
|
-
if (!existsSync(from) || existsSync(to)) continue;
|
|
24
|
-
mkdirSync(dirname(to), { recursive: true });
|
|
25
|
-
copyFileSync(from, to);
|
|
26
|
-
}
|
|
27
17
|
}
|
package/src/tui/App.jsx
CHANGED
|
@@ -844,6 +844,14 @@ function toolHasDisplayResultForRows(item) {
|
|
|
844
844
|
return true;
|
|
845
845
|
}
|
|
846
846
|
|
|
847
|
+
// Mirror ToolExecution expanded ResultBody rawText selection (aggregate always rawResult).
|
|
848
|
+
function toolExpandedRawTextForRows(item, rawRt) {
|
|
849
|
+
if (item?.aggregate) return rawRt;
|
|
850
|
+
const hasDisplayResult = toolHasDisplayResultForRows(item);
|
|
851
|
+
if (hasDisplayResult) return toolDisplayedResultTextForRows(item);
|
|
852
|
+
return rawRt;
|
|
853
|
+
}
|
|
854
|
+
|
|
847
855
|
function toolHeaderFailureOnlyForRows(item, normalizedName, hasDisplayResult) {
|
|
848
856
|
if (hasDisplayResult) return false;
|
|
849
857
|
const bgArgs = backgroundArgsForRows(item.args);
|
|
@@ -876,10 +884,16 @@ function toolArgPathForRows(item) {
|
|
|
876
884
|
return a?.path ?? a?.file_path ?? a?.file ?? '';
|
|
877
885
|
}
|
|
878
886
|
|
|
879
|
-
function isShellSurfaceForRows(normalizedName) {
|
|
887
|
+
function isShellSurfaceForRows(normalizedName, label = '') {
|
|
880
888
|
const n = String(normalizedName || '').toLowerCase();
|
|
889
|
+
const l = String(label || '').toLowerCase();
|
|
881
890
|
return n === 'shell' || n === 'bash' || n === 'bash_session'
|
|
882
|
-
|| n === 'shell_command' || n === 'job_wait';
|
|
891
|
+
|| n === 'shell_command' || n === 'job_wait' || l === 'run';
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function isShellSurfaceForToolItem(item, normalizedName) {
|
|
895
|
+
const label = formatToolSurface(item?.name, item?.args)?.label || '';
|
|
896
|
+
return isShellSurfaceForRows(normalizedName, label);
|
|
883
897
|
}
|
|
884
898
|
|
|
885
899
|
// EXPANDED tool bodies are post-processed by formatExpandedResult (JSON pretty,
|
|
@@ -892,7 +906,7 @@ function estimateToolRenderedResultRows(value, { pathArg = '', isShell = false,
|
|
|
892
906
|
if (!text) return 1;
|
|
893
907
|
try {
|
|
894
908
|
const logical = formatExpandedResult(text, { pathArg, isShell });
|
|
895
|
-
const rows = wrapExpandedResultLines(logical, columns).length;
|
|
909
|
+
const rows = wrapExpandedResultLines(logical, columns, { isShell }).length;
|
|
896
910
|
return Math.max(1, rows);
|
|
897
911
|
} catch {
|
|
898
912
|
return Math.max(1, text.split('\n').length);
|
|
@@ -961,10 +975,11 @@ function estimateTranscriptItemRows(item, columns, toolOutputExpanded) {
|
|
|
961
975
|
// Aggregate cards pass no pathArg/isShell to ResultBody; non-aggregate
|
|
962
976
|
// cards do. Mirror that here so the formatExpandedResult row count matches
|
|
963
977
|
// the rendered body exactly (JSON pretty / line-number split can shift it).
|
|
978
|
+
const estimateText = toolExpandedRawTextForRows(item, rawRt);
|
|
964
979
|
const rawOpts = item.aggregate
|
|
965
980
|
? {}
|
|
966
|
-
: { pathArg: toolArgPathForRows(item), isShell:
|
|
967
|
-
return TOOL_MARGIN_TOP + 1 + estimateToolRenderedResultRows(
|
|
981
|
+
: { pathArg: toolArgPathForRows(item), isShell: isShellSurfaceForToolItem(item, normalizedName) };
|
|
982
|
+
return TOOL_MARGIN_TOP + 1 + estimateToolRenderedResultRows(estimateText, { ...rawOpts, columns });
|
|
968
983
|
}
|
|
969
984
|
// Expanded agent card with no raw body to reveal: ToolExecution still
|
|
970
985
|
// shows one agent-brief detail line, so margin + header + one detail row.
|
|
@@ -999,7 +1014,7 @@ function estimateTranscriptItemRows(item, columns, toolOutputExpanded) {
|
|
|
999
1014
|
const resultText = backgroundMeta?.hasResponse ? backgroundMeta.body : rt;
|
|
1000
1015
|
const resultRows = estimateToolRenderedResultRows(resultText, {
|
|
1001
1016
|
pathArg: toolArgPathForRows(item),
|
|
1002
|
-
isShell:
|
|
1017
|
+
isShell: isShellSurfaceForToolItem(item, normalizedName),
|
|
1003
1018
|
columns,
|
|
1004
1019
|
});
|
|
1005
1020
|
return TOOL_MARGIN_TOP + 1 + resultRows;
|
|
@@ -1970,13 +1985,21 @@ export function App({ store, initialStatusLine = '' }) {
|
|
|
1970
1985
|
return clipped;
|
|
1971
1986
|
}, [selectionClip]);
|
|
1972
1987
|
|
|
1973
|
-
const paintSelectionRect = useCallback((clippedRect, { rememberText = true } = {}) => {
|
|
1988
|
+
const paintSelectionRect = useCallback((clippedRect, { rememberText = true, immediate = false } = {}) => {
|
|
1974
1989
|
const nextRect = clippedRect || null;
|
|
1975
1990
|
const state = selectionPaintRef.current;
|
|
1976
|
-
if (selectionRectsEqual(state.rect, nextRect))
|
|
1991
|
+
if (selectionRectsEqual(state.rect, nextRect)) {
|
|
1992
|
+
const needsCapture = nextRect && rememberText && nextRect.captureText !== false;
|
|
1993
|
+
if (!immediate && !needsCapture) return false;
|
|
1994
|
+
if (immediate || needsCapture) {
|
|
1995
|
+
store.setRenderSelection?.(nextRect, { immediate: true });
|
|
1996
|
+
}
|
|
1997
|
+
if (needsCapture) rememberSelectionTextSoon();
|
|
1998
|
+
return true;
|
|
1999
|
+
}
|
|
1977
2000
|
state.rect = nextRect;
|
|
1978
2001
|
state.t = Date.now();
|
|
1979
|
-
store.setRenderSelection?.(nextRect);
|
|
2002
|
+
store.setRenderSelection?.(nextRect, immediate ? { immediate: true } : undefined);
|
|
1980
2003
|
if (nextRect && rememberText && nextRect.captureText !== false) rememberSelectionTextSoon();
|
|
1981
2004
|
return true;
|
|
1982
2005
|
}, [store, rememberSelectionTextSoon]);
|
|
@@ -1991,7 +2014,7 @@ export function App({ store, initialStatusLine = '' }) {
|
|
|
1991
2014
|
state.timer = null;
|
|
1992
2015
|
state.pending = null;
|
|
1993
2016
|
}
|
|
1994
|
-
paintSelectionRect(clippedRect, { rememberText: true });
|
|
2017
|
+
paintSelectionRect(clippedRect, { rememberText: true, immediate: true });
|
|
1995
2018
|
}, [paintSelectionRect, withSelectionClip]);
|
|
1996
2019
|
|
|
1997
2020
|
const applySelectionRectThrottled = useCallback((rect) => {
|
|
@@ -2348,7 +2371,7 @@ export function App({ store, initialStatusLine = '' }) {
|
|
|
2348
2371
|
// Finalize the prompt selection; highlight persists (copy on Ctrl+C).
|
|
2349
2372
|
const offset = promptOffsetAt(x, y);
|
|
2350
2373
|
dragRef.current.active = false;
|
|
2351
|
-
|
|
2374
|
+
promptMouseSelectionRef.current?.extendTo?.(offset, true);
|
|
2352
2375
|
continue;
|
|
2353
2376
|
}
|
|
2354
2377
|
// Button release while dragging: finalize with the release coordinate
|
|
@@ -7615,7 +7638,7 @@ export function App({ store, initialStatusLine = '' }) {
|
|
|
7615
7638
|
if (deltaY === 0) return;
|
|
7616
7639
|
const clippedRect = withSelectionClip(shiftSelectionRectY(dragRef.current.rect, deltaY));
|
|
7617
7640
|
dragRef.current = { ...dragRef.current, rect: clippedRect };
|
|
7618
|
-
paintSelectionRect(clippedRect, { rememberText: true });
|
|
7641
|
+
paintSelectionRect(clippedRect, { rememberText: true, immediate: true });
|
|
7619
7642
|
}, [viewportHeight, transcriptWindow.totalRows, transcriptWindow.effectiveScrollOffset, withSelectionClip, paintSelectionRect]);
|
|
7620
7643
|
useEffect(() => {
|
|
7621
7644
|
const maxRows = Math.max(0, Number(transcriptWindow.maxScrollRows) || 0);
|
|
@@ -52,6 +52,9 @@ function hintStyle(tone) {
|
|
|
52
52
|
// paddingX of 1, so the typing start can sit directly against that padding
|
|
53
53
|
// without an extra guard column.
|
|
54
54
|
const IME_LEFT_GUARD_COLUMNS = 0;
|
|
55
|
+
// Coalesce prompt mouse-drag extend commits (SGR motion can fire faster than ink
|
|
56
|
+
// needs to immediate-render). Matches transcript selection paint cadence.
|
|
57
|
+
const MOUSE_EXTEND_COALESCE_MS = 24;
|
|
55
58
|
|
|
56
59
|
function insertText(draft, input) {
|
|
57
60
|
if (!input) return draft;
|
|
@@ -146,6 +149,7 @@ export function PromptInput({
|
|
|
146
149
|
const cursorEnabledRef = useRef(false); // latest enabled state, read by the anchor fn at render time
|
|
147
150
|
const contentWidthRef = useRef(80);
|
|
148
151
|
const preferredColumnRef = useRef(null);
|
|
152
|
+
const mouseExtendCoalesceRef = useRef({ pendingNext: null, timer: null, t: 0 });
|
|
149
153
|
const { value, cursor } = draft;
|
|
150
154
|
draftRef.current = draft;
|
|
151
155
|
if (selectionRef) {
|
|
@@ -237,6 +241,49 @@ export function PromptInput({
|
|
|
237
241
|
commitDraft(fn(draftRef.current), options);
|
|
238
242
|
};
|
|
239
243
|
|
|
244
|
+
const cancelMouseExtendCoalesce = () => {
|
|
245
|
+
const state = mouseExtendCoalesceRef.current;
|
|
246
|
+
if (state.timer) {
|
|
247
|
+
clearTimeout(state.timer);
|
|
248
|
+
state.timer = null;
|
|
249
|
+
}
|
|
250
|
+
state.pendingNext = null;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const queueMouseExtendCommit = (next, immediate = false) => {
|
|
254
|
+
if (immediate) {
|
|
255
|
+
cancelMouseExtendCoalesce();
|
|
256
|
+
commitDraft(next);
|
|
257
|
+
mouseExtendCoalesceRef.current.t = Date.now();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (draftStateEqual(draftRef.current, next)) {
|
|
261
|
+
cancelMouseExtendCoalesce();
|
|
262
|
+
commitDraft(next);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const state = mouseExtendCoalesceRef.current;
|
|
266
|
+
state.pendingNext = next;
|
|
267
|
+
const now = Date.now();
|
|
268
|
+
const elapsed = now - state.t;
|
|
269
|
+
if (elapsed >= MOUSE_EXTEND_COALESCE_MS) {
|
|
270
|
+
cancelMouseExtendCoalesce();
|
|
271
|
+
state.t = now;
|
|
272
|
+
commitDraft(next);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (state.timer) return;
|
|
276
|
+
state.timer = setTimeout(() => {
|
|
277
|
+
const current = mouseExtendCoalesceRef.current;
|
|
278
|
+
const pending = current.pendingNext;
|
|
279
|
+
current.timer = null;
|
|
280
|
+
current.pendingNext = null;
|
|
281
|
+
current.t = Date.now();
|
|
282
|
+
if (pending) commitDraft(pending);
|
|
283
|
+
}, Math.max(1, MOUSE_EXTEND_COALESCE_MS - elapsed));
|
|
284
|
+
state.timer.unref?.();
|
|
285
|
+
};
|
|
286
|
+
|
|
240
287
|
// [mixdog] Mouse drag-selection driver. App's single mouse handler maps a
|
|
241
288
|
// click/drag cell over the prompt box to an edit offset and calls these so the
|
|
242
289
|
// SAME selectionAnchor/cursor engine that keyboard Shift-selection uses paints
|
|
@@ -246,22 +293,36 @@ export function PromptInput({
|
|
|
246
293
|
mouseSelectionRef.current = {
|
|
247
294
|
offsetAtCell: (row, col) => offsetAtCell(draftRef.current.value, row, col, contentWidthRef.current),
|
|
248
295
|
anchorAt: (offset) => {
|
|
296
|
+
cancelMouseExtendCoalesce();
|
|
249
297
|
const value = draftRef.current.value;
|
|
250
298
|
const off = Math.max(0, Math.min(value.length, Math.floor(Number(offset) || 0)));
|
|
251
299
|
commitDraft({ ...draftRef.current, cursor: off, selectionAnchor: off });
|
|
300
|
+
mouseExtendCoalesceRef.current.t = Date.now();
|
|
252
301
|
},
|
|
253
|
-
extendTo: (offset) => {
|
|
302
|
+
extendTo: (offset, immediate = false) => {
|
|
303
|
+
if (offset == null) {
|
|
304
|
+
if (immediate) cancelMouseExtendCoalesce();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
254
307
|
const d = draftRef.current;
|
|
255
308
|
const off = Math.max(0, Math.min(d.value.length, Math.floor(Number(offset) || 0)));
|
|
256
309
|
const anchor = Number.isFinite(d.selectionAnchor) ? d.selectionAnchor : d.cursor;
|
|
257
|
-
|
|
310
|
+
queueMouseExtendCommit({ ...d, cursor: off, selectionAnchor: anchor }, immediate);
|
|
258
311
|
},
|
|
259
312
|
clear: () => {
|
|
313
|
+
cancelMouseExtendCoalesce();
|
|
260
314
|
if (selectionRange(draftRef.current)) commitDraft(clearSelection(draftRef.current));
|
|
261
315
|
},
|
|
262
316
|
};
|
|
263
317
|
}
|
|
264
318
|
|
|
319
|
+
useEffect(() => () => {
|
|
320
|
+
const state = mouseExtendCoalesceRef.current;
|
|
321
|
+
if (state.timer) clearTimeout(state.timer);
|
|
322
|
+
state.timer = null;
|
|
323
|
+
state.pendingNext = null;
|
|
324
|
+
}, []);
|
|
325
|
+
|
|
265
326
|
const moveDraftVertically = (direction, { extend = false } = {}) => {
|
|
266
327
|
const current = draftRef.current;
|
|
267
328
|
const moved = verticalOffset(
|
|
@@ -98,10 +98,15 @@ function renderDeltaText(text) {
|
|
|
98
98
|
// renderDeltaText.
|
|
99
99
|
// - EXPANDED (raw=true): formatExpandedResult then wrapExpandedResultLines so
|
|
100
100
|
// each physical row fits the body width before render (rail rows stay 1:1;
|
|
101
|
-
// ink does not re-wrap).
|
|
101
|
+
// ink does not re-wrap). Physical row mount cap: MIXDOG_TUI_TOOL_OUTPUT_MAX_RENDER_LINES
|
|
102
|
+
// (default 600; 0 disables). Shell/script bodies keep the newest tail when capped.
|
|
102
103
|
function ResultBody({ lines, rawText, pathArg = '', isShell = false, columns, color, raw }) {
|
|
103
104
|
const renderLines = raw
|
|
104
|
-
? wrapExpandedResultLines(
|
|
105
|
+
? wrapExpandedResultLines(
|
|
106
|
+
formatExpandedResult(rawText, { pathArg, isShell }),
|
|
107
|
+
columns,
|
|
108
|
+
{ isShell },
|
|
109
|
+
)
|
|
105
110
|
: (lines || []);
|
|
106
111
|
if (!renderLines || renderLines.length === 0) return null;
|
|
107
112
|
return (
|