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.
Files changed (57) hide show
  1. package/package.json +3 -3
  2. package/scripts/session-ingest-smoke.mjs +2 -2
  3. package/src/headless-role.mjs +1 -1
  4. package/src/lib/mixdog-debug.cjs +0 -22
  5. package/src/lib/plugin-paths.cjs +1 -7
  6. package/src/lib/rules-builder.cjs +2 -2
  7. package/src/mixdog-session-runtime.mjs +0 -1
  8. package/src/repl.mjs +0 -2
  9. package/src/runtime/agent/orchestrator/internal-roles.mjs +1 -1
  10. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +27 -49
  11. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +5 -20
  12. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +8 -27
  13. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +52 -182
  14. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +8 -30
  15. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +258 -0
  16. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +0 -8
  18. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
  19. package/src/runtime/channels/index.mjs +0 -30
  20. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  21. package/src/runtime/channels/lib/config.mjs +0 -1
  22. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  23. package/src/runtime/channels/lib/executor.mjs +0 -3
  24. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  25. package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
  26. package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
  27. package/src/runtime/channels/lib/session-discovery.mjs +0 -4
  28. package/src/runtime/channels/lib/tool-format.mjs +0 -1
  29. package/src/runtime/channels/lib/transcript-discovery.mjs +1 -10
  30. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  31. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  32. package/src/runtime/memory/index.mjs +3 -24
  33. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  34. package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
  35. package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
  36. package/src/runtime/memory/lib/session-ingest.mjs +9 -7
  37. package/src/runtime/search/index.mjs +2 -7
  38. package/src/runtime/search/lib/config.mjs +0 -4
  39. package/src/runtime/search/lib/state.mjs +1 -15
  40. package/src/runtime/search/lib/web-tools.mjs +0 -1
  41. package/src/runtime/shared/child-spawn-gate.mjs +0 -6
  42. package/src/standalone/seeds.mjs +1 -11
  43. package/src/tui/App.jsx +35 -12
  44. package/src/tui/components/PromptInput.jsx +63 -2
  45. package/src/tui/components/ToolExecution.jsx +7 -2
  46. package/src/tui/components/tool-output-format.mjs +156 -22
  47. package/src/tui/components/tool-output-format.test.mjs +93 -1
  48. package/src/tui/dist/index.mjs +473 -116
  49. package/src/tui/markdown/format-token.mjs +267 -108
  50. package/src/tui/markdown/format-token.test.mjs +105 -9
  51. package/src/tui/theme.mjs +10 -0
  52. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
  53. package/vendor/ink/build/ink.js +54 -8
  54. package/src/hooks/lib/permission-rules.cjs +0 -170
  55. package/src/hooks/lib/settings-loader.cjs +0 -112
  56. package/src/lib/hook-pipe-path.cjs +0 -10
  57. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
@@ -181,6 +181,11 @@ export default class Ink {
181
181
  // cells ({ x1, y1, x2, y2 } inclusive, normalized) or null. The App sets it
182
182
  // via setSelection(); onRender forwards it to the renderer for highlighting.
183
183
  selectionRect = null;
184
+ // [mixdog fork] Coalesce drag-selection repaints when a frame is pending.
185
+ isRendering = false;
186
+ selectionRepaintQueued = false;
187
+ selectionRepaintFlushPending = false;
188
+ selectionRepaintEpoch = 0;
184
189
  // [mixdog fork] text under the current selection rect, refreshed every render
185
190
  // from the output grid. Read back via getSelectionText() on drag-release.
186
191
  selectedText = null;
@@ -318,9 +323,9 @@ export default class Ink {
318
323
  this.log.setCursorPosition(position);
319
324
  };
320
325
  // [mixdog fork] Update the mouse drag-selection rectangle and repaint so the
321
- // inverse highlight tracks the drag live. Called by the App's mouse handler.
326
+ // inverse highlight tracks the drag. Called by the App's mouse handler.
322
327
  // A no-op-equal update is skipped to avoid redundant frames during motion.
323
- setSelection = (rect) => {
328
+ setSelection = (rect, options = {}) => {
324
329
  const a = this.selectionRect;
325
330
  const same = a === rect ||
326
331
  (a && rect &&
@@ -333,16 +338,47 @@ export default class Ink {
333
338
  a.clipY2 === rect.clipY2 &&
334
339
  a.captureText === rect.captureText);
335
340
  if (same) {
336
- return;
341
+ if (!options.immediate) {
342
+ return;
343
+ }
344
+ }
345
+ else {
346
+ this.selectionRect = rect ?? null;
337
347
  }
338
- this.selectionRect = rect ?? null;
339
348
  if (!this.isUnmounted) {
340
- // Drag selection is a direct manipulation gesture. Route it through
341
- // the unthrottled renderer so the highlight tracks the pointer
342
- // instead of trailing behind at maxFps.
343
- this.rootNode.onImmediateRender();
349
+ if (options.immediate) {
350
+ this.selectionRepaintEpoch++;
351
+ this.selectionRepaintFlushPending = false;
352
+ this.rootNode.onImmediateRender();
353
+ return;
354
+ }
355
+ this.scheduleSelectionRepaint();
344
356
  }
345
357
  };
358
+ scheduleSelectionRepaint = () => {
359
+ if (this.hasPendingThrottledRender) {
360
+ return;
361
+ }
362
+ if (this.isRendering) {
363
+ this.selectionRepaintQueued = true;
364
+ return;
365
+ }
366
+ if (this.selectionRepaintFlushPending) {
367
+ return;
368
+ }
369
+ this.selectionRepaintFlushPending = true;
370
+ const epoch = ++this.selectionRepaintEpoch;
371
+ queueMicrotask(() => {
372
+ this.selectionRepaintFlushPending = false;
373
+ if (epoch !== this.selectionRepaintEpoch || this.isUnmounted) {
374
+ return;
375
+ }
376
+ if (this.hasPendingThrottledRender) {
377
+ return;
378
+ }
379
+ this.rootNode.onImmediateRender();
380
+ });
381
+ };
346
382
  // [mixdog fork] Given a 0-based cell (x, y), return the inclusive rect of the
347
383
  // word (maximal run of non-whitespace cells) on that single row, or null if
348
384
  // the cell is whitespace/empty or out of range. Reuses the cached cell-value
@@ -401,6 +437,8 @@ export default class Ink {
401
437
  this.nextRenderCommit.resolve();
402
438
  this.nextRenderCommit = undefined;
403
439
  }
440
+ this.isRendering = true;
441
+ try {
404
442
  const startTime = performance.now();
405
443
  const { output, outputHeight, staticOutput, cursor, selectedText, plainRows } = render(this.rootNode, this.isScreenReaderEnabled, this.selectionRect);
406
444
  // [mixdog fork] Cache the text under the current selection rect so the App
@@ -488,6 +526,14 @@ export default class Ink {
488
526
  this.fullStaticOutput += staticOutput;
489
527
  }
490
528
  this.renderInteractiveFrame(output, outputHeight, hasStaticOutput ? staticOutput : '');
529
+ }
530
+ finally {
531
+ this.isRendering = false;
532
+ if (this.selectionRepaintQueued) {
533
+ this.selectionRepaintQueued = false;
534
+ this.scheduleSelectionRepaint();
535
+ }
536
+ }
491
537
  };
492
538
  render(node) {
493
539
  const tree = (React.createElement(AccessibilityContext.Provider, { value: { isScreenReaderEnabled: this.isScreenReaderEnabled } },
@@ -1,170 +0,0 @@
1
- 'use strict';
2
- /**
3
- * permission-rules.cjs
4
- * Pattern matcher and priority evaluator. Priority order and Tool(content)
5
- * parsing mirror Claude Code native logic; specifier matching uses a local
6
- * path/command glob (native delegates per-tool content matching to each tool).
7
- *
8
- * Priority (highest → lowest): deny > ask > allow > mode default
9
- *
10
- * Pattern forms:
11
- * mcp__* → toolName starts with "mcp__"
12
- * ToolName → exact toolName match (no parens)
13
- * ToolName(*) → toolName matches AND specifier glob matches any
14
- * ToolName(specifier) → toolName matches AND specifier glob matches
15
- * toolInput.path / .command / .file
16
- * Glob chars: * (any chars within segment), ** (any segments)
17
- *
18
- * No external deps; no eval/Function.
19
- */
20
-
21
- // Read-only tool set (mixdog tool names).
22
- const READ_ONLY_TOOLS = new Set([
23
- 'read', 'list', 'glob', 'grep', 'find_symbol', 'find_references', 'find_callers', 'explore',
24
- 'recall', 'search', 'fetch', 'web_fetch',
25
- 'schedule_status', 'list_models', 'job_wait', 'download_attachment',
26
- ]);
27
-
28
- /**
29
- * Find the index of the first/last UNescaped occurrence of a char.
30
- * A char is escaped when preceded by an odd number of backslashes.
31
- * Mirrors native permissionRuleParser.findFirst/LastUnescapedChar.
32
- */
33
- function findFirstUnescapedChar(str, char) {
34
- for (let i = 0; i < str.length; i++) {
35
- if (str[i] !== char) continue;
36
- let bs = 0, j = i - 1;
37
- while (j >= 0 && str[j] === '\\') { bs++; j--; }
38
- if (bs % 2 === 0) return i;
39
- }
40
- return -1;
41
- }
42
-
43
- function findLastUnescapedChar(str, char) {
44
- for (let i = str.length - 1; i >= 0; i--) {
45
- if (str[i] !== char) continue;
46
- let bs = 0, j = i - 1;
47
- while (j >= 0 && str[j] === '\\') { bs++; j--; }
48
- if (bs % 2 === 0) return i;
49
- }
50
- return -1;
51
- }
52
-
53
- /** Reverse escapeRuleContent: \( -> (, \) -> ), \\ -> \ (order matters). */
54
- function unescapeRuleContent(content) {
55
- return content
56
- .replace(/\\\(/g, '(')
57
- .replace(/\\\)/g, ')')
58
- .replace(/\\\\/g, '\\');
59
- }
60
-
61
- /**
62
- * Convert a glob pattern string to a RegExp.
63
- * Supports: * (non-slash wildcard), ** (any), ? (single char).
64
- * Safe: no eval.
65
- */
66
- function globToRegex(glob) {
67
- let src = '';
68
- let i = 0;
69
- while (i < glob.length) {
70
- const ch = glob[i];
71
- if (ch === '*' && glob[i + 1] === '*') {
72
- src += '.*';
73
- i += 2;
74
- if (glob[i] === '/') i++; // consume trailing slash
75
- } else if (ch === '*') {
76
- src += '[^/\\\\]*';
77
- i++;
78
- } else if (ch === '?') {
79
- src += '[^/\\\\]';
80
- i++;
81
- } else {
82
- // Escape regex metacharacters
83
- src += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&');
84
- i++;
85
- }
86
- }
87
- return new RegExp('^' + src + '$', 'i');
88
- }
89
-
90
- /**
91
- * Match a single pattern entry against (toolName, toolInput).
92
- * Returns true if the pattern applies.
93
- */
94
- function matchesPattern(pattern, toolName, toolInput) {
95
- if (typeof pattern !== 'string') return false;
96
-
97
- // Detect Tool(specifier) form. Escape-aware, mirroring native
98
- // permissionRuleValueFromString: only UNescaped parens delimit the
99
- // specifier, and the close paren must be the final char.
100
- const parenIdx = findFirstUnescapedChar(pattern, '(');
101
- const closeIdx = findLastUnescapedChar(pattern, ')');
102
- if (parenIdx > 0 && closeIdx === pattern.length - 1 && closeIdx > parenIdx) {
103
- const namePart = pattern.slice(0, parenIdx);
104
- const rawSpecifier = pattern.slice(parenIdx + 1, closeIdx);
105
-
106
- if (toolName !== namePart) return false;
107
-
108
- // Empty "()" or wildcard "(*)" specifier → tool-wide rule, match any
109
- if (rawSpecifier === '' || rawSpecifier === '*') return true;
110
-
111
- // Unescape \( \) before glob matching
112
- const specifier = unescapeRuleContent(rawSpecifier);
113
- // Match specifier against known path-bearing fields
114
- const specRe = globToRegex(specifier);
115
- const inp = toolInput || {};
116
- const candidates = [...new Set([
117
- inp.path, inp.command, inp.file, inp.file_path, inp.base_path, inp.cwd,
118
- ...(Array.isArray(inp.path) ? inp.path.map(p => (p && typeof p === 'object' ? p.path : p)) : []),
119
- ...(Array.isArray(inp.reads) ? inp.reads.map(r => r?.path) : []),
120
- ...(Array.isArray(inp.edits) ? inp.edits.flatMap(e => [e?.path, e?.file_path]) : []),
121
- ...(Array.isArray(inp.writes) ? inp.writes.flatMap(w => [w?.path, w?.file_path]) : []),
122
- ].filter(v => typeof v === 'string'))];
123
-
124
- return candidates.some(c => specRe.test(c));
125
- }
126
-
127
- // Prefix wildcard: e.g. "mcp__*"
128
- if (pattern.endsWith('*')) {
129
- const prefix = pattern.slice(0, -1);
130
- return toolName.startsWith(prefix);
131
- }
132
-
133
- // Glob on toolName itself (may contain * or **)
134
- if (pattern.includes('*') || pattern.includes('?')) {
135
- return globToRegex(pattern).test(toolName);
136
- }
137
-
138
- // Exact match
139
- return toolName === pattern;
140
- }
141
-
142
- /**
143
- * Evaluate priority rules against a tool call.
144
- *
145
- * @param {string} toolName
146
- * @param {object} toolInput
147
- * @param {string[]} allowList
148
- * @param {string[]} denyList
149
- * @param {string[]} askList
150
- * @returns {'deny'|'ask'|'allow'|null} null = no list matched (use mode default)
151
- */
152
- function evaluateRules(toolName, toolInput, allowList, denyList, askList) {
153
- if (denyList.some(p => matchesPattern(p, toolName, toolInput))) return 'deny';
154
- if (askList.some(p => matchesPattern(p, toolName, toolInput))) return 'ask';
155
- if (allowList.some(p => matchesPattern(p, toolName, toolInput))) return 'allow';
156
- return null;
157
- }
158
-
159
- /**
160
- * Determine whether a tool is considered read-only.
161
- * For mcp__ tools: strip the mcp__ prefix chain and check the short tool name.
162
- */
163
- function isReadOnlyTool(toolName) {
164
- // Strip mcp__plugin_...__ prefix (one or more double-underscore segments)
165
- const parts = toolName.split('__');
166
- const shortName = parts[parts.length - 1];
167
- return READ_ONLY_TOOLS.has(shortName);
168
- }
169
-
170
- module.exports = { matchesPattern, evaluateRules, isReadOnlyTool, READ_ONLY_TOOLS };
@@ -1,112 +0,0 @@
1
- 'use strict';
2
- /**
3
- * settings-loader.cjs
4
- * Loads and merges Mixdog settings from three tiers (lowest → highest):
5
- * 1. User global: $MIXDOG_CONFIG_DIR/settings.json (or ~/.mixdog/settings.json)
6
- * 2. Project: $MIXDOG_PROJECT_DIR/.mixdog/settings.json (or cwd/.mixdog/settings.json)
7
- * 3. Project local: $MIXDOG_PROJECT_DIR/.mixdog/settings.local.json
8
- *
9
- * Only `permissions` sub-tree is returned; the rest is ignored.
10
- * Pure fs/path — no external deps.
11
- */
12
-
13
- const fs = require('fs');
14
- const path = require('path');
15
- const os = require('os');
16
-
17
- // ── In-process mtime-driven cache ────────────────────────────────────────────
18
- // Keyed by resolved file path. Each entry: { mtime: number|null, data: any }.
19
- // Invalidated per-tier independently when mtime changes.
20
- // TTL: never-expire by default (mtime-driven only).
21
- const _fileCache = new Map();
22
-
23
- function readJsonCached(filePath) {
24
- let entry = _fileCache.get(filePath);
25
- let mtime = null;
26
- try { mtime = fs.statSync(filePath).mtimeMs; } catch { /* file absent */ }
27
- if (entry && entry.mtime === mtime) return entry.data;
28
- let data = null;
29
- if (mtime !== null) {
30
- try { data = JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { data = null; }
31
- }
32
- _fileCache.set(filePath, { mtime, data });
33
- return data;
34
- }
35
-
36
- /** Clear the entire settings cache (for test isolation). */
37
- function clearSettingsCache() {
38
- _fileCache.clear();
39
- }
40
-
41
- /**
42
- * Merge two permissions objects (base then overlay).
43
- * Arrays are concatenated and de-duped; scalar fields are overwritten.
44
- */
45
- function mergePermissions(base, overlay) {
46
- if (!overlay) return base || {};
47
- if (!base) return overlay || {};
48
-
49
- const merged = Object.assign({}, base);
50
-
51
- for (const key of ['allow', 'deny', 'ask']) {
52
- const b = Array.isArray(base[key]) ? base[key] : [];
53
- const o = Array.isArray(overlay[key]) ? overlay[key] : [];
54
- const combined = [...b, ...o];
55
- if (combined.length) merged[key] = [...new Set(combined)];
56
- }
57
-
58
- // scalar: overlay wins
59
- if (overlay.defaultMode !== undefined) merged.defaultMode = overlay.defaultMode;
60
-
61
- return merged;
62
- }
63
-
64
- /**
65
- * Load and merge settings from all three tiers.
66
- * Returns `{ allow: string[], deny: string[], ask: string[], defaultMode: string }`.
67
- */
68
- function loadPermissions(projectDir) {
69
- const userConfigDir = process.env.MIXDOG_CONFIG_DIR ||
70
- path.join(os.homedir(), '.mixdog');
71
- const projDir = projectDir ||
72
- process.env.MIXDOG_PROJECT_DIR ||
73
- process.cwd();
74
-
75
- const userSettings = readJsonCached(path.join(userConfigDir, 'settings.json'));
76
- const projectSettings = readJsonCached(path.join(projDir, '.mixdog', 'settings.json'));
77
- const localSettings = readJsonCached(path.join(projDir, '.mixdog', 'settings.local.json'));
78
-
79
- // Accept permissions from `settings.permissions` (canonical) OR top-level
80
- // `allow`/`deny`/`ask`/`defaultMode` fields (common user shorthand).
81
- function extractPerms(s) {
82
- if (!s) return {};
83
- const nested = (s.permissions && typeof s.permissions === 'object') ? s.permissions : {};
84
- const topLevel = {};
85
- for (const key of ['allow', 'deny', 'ask']) {
86
- if (Array.isArray(s[key])) topLevel[key] = s[key];
87
- }
88
- if (typeof s.defaultMode === 'string') topLevel.defaultMode = s.defaultMode;
89
- return mergePermissions(nested, topLevel);
90
- }
91
- const userPerms = extractPerms(userSettings);
92
- const projectPerms = extractPerms(projectSettings);
93
- const localPerms = extractPerms(localSettings);
94
-
95
- let merged = mergePermissions({}, userPerms);
96
- merged = mergePermissions(merged, projectPerms);
97
- merged = mergePermissions(merged, localPerms);
98
-
99
- const VALID_MODES = new Set(['default', 'acceptEdits', 'plan', 'dontAsk', 'bypassPermissions', 'auto']);
100
- const rawMode = merged.defaultMode;
101
- const resolvedMode = (typeof rawMode === 'string' && VALID_MODES.has(rawMode))
102
- ? rawMode
103
- : 'default';
104
- return {
105
- allow: Array.isArray(merged.allow) ? merged.allow : [],
106
- deny: Array.isArray(merged.deny) ? merged.deny : [],
107
- ask: Array.isArray(merged.ask) ? merged.ask : [],
108
- defaultMode: resolvedMode,
109
- };
110
- }
111
-
112
- module.exports = { loadPermissions, clearSettingsCache };
@@ -1,10 +0,0 @@
1
- 'use strict';
2
-
3
- // Single source for the hook IPC path. Centralizes an existing platform
4
- // contract (not new heuristic). Rust mirror native/mixdog-shim/src/main.rs:34-43
5
- // must stay in sync.
6
- module.exports = function hookPipePath() {
7
- return process.platform === 'win32'
8
- ? '\\\\.\\pipe\\mixdog-hooks'
9
- : require('path').join(process.env.XDG_RUNTIME_DIR || '/tmp', 'mixdog-hooks.sock');
10
- };