mixdog 0.9.97 → 0.9.99

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 (48) hide show
  1. package/package.json +9 -2
  2. package/src/rules/agent/30-explorer.md +43 -32
  3. package/src/runtime/agent/orchestrator/agent-trace.mjs +19 -5
  4. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +7 -0
  5. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -3
  6. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +5 -3
  7. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +23 -1
  8. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +2 -1
  9. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +44 -6
  10. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -1
  11. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +3 -6
  12. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +103 -12
  13. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +10 -30
  14. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -84
  15. package/src/runtime/attachments/pdf-extract.mjs +83 -0
  16. package/src/runtime/attachments/store.mjs +515 -0
  17. package/src/runtime/attachments/store.test.mjs +188 -0
  18. package/src/runtime/channels/lib/interaction-handlers.mjs +1 -191
  19. package/src/runtime/channels/lib/runtime-paths.mjs +1 -4
  20. package/src/runtime/channels/lib/worker-ipc.mjs +0 -66
  21. package/src/runtime/channels/lib/worker-main.mjs +1 -20
  22. package/src/runtime/memory/lib/http-wire.mjs +39 -4
  23. package/src/runtime/shared/json-metrics.mjs +94 -0
  24. package/src/runtime/shared/turn-snapshot.mjs +13 -0
  25. package/src/runtime/shared/turn-worktree-snapshot.mjs +19 -0
  26. package/src/session-runtime/provider-models.mjs +11 -1
  27. package/src/session-runtime/quick-model-rows.mjs +38 -7
  28. package/src/session-runtime/quick-search-models.mjs +15 -17
  29. package/src/session-runtime/runtime-core.mjs +9 -4
  30. package/src/session-runtime/runtime-tunables.mjs +9 -6
  31. package/src/session-runtime/session-title.mjs +27 -3
  32. package/src/standalone/channel-transport.mjs +28 -12
  33. package/src/standalone/daemon.mjs +3 -0
  34. package/src/standalone/session-protocol.mjs +1 -0
  35. package/src/standalone/session-service.mjs +23 -1
  36. package/src/standalone/session-transport.mjs +3 -2
  37. package/src/tui/app/prompt-submit.mjs +7 -9
  38. package/src/tui/app/use-prompt-handlers.mjs +7 -3
  39. package/src/tui/dist/index.mjs +249 -43
  40. package/src/tui/paste-attachments.mjs +13 -7
  41. package/src/tui/prompt-history-store.mjs +52 -4
  42. package/src/tui/session/agent-job-feed.mjs +5 -1
  43. package/src/tui/session/live-share.mjs +21 -7
  44. package/src/tui/session/queue-helpers.mjs +6 -6
  45. package/src/tui/session/session-api-ext.mjs +12 -4
  46. package/src/tui/session/session-api.mjs +10 -1
  47. package/src/tui/session/session-flow.mjs +4 -2
  48. package/src/tui/session-local.mjs +15 -5
@@ -299,14 +299,20 @@ export function createLiveShare({
299
299
  try { socket.destroy(); } catch { /* already gone */ }
300
300
  });
301
301
  attachLineReader(socket, (frame) => {
302
- if (frame.t === 'submit' && typeof frame.text === 'string' && frame.text.trim()) {
302
+ if (frame.t === 'submit' && (
303
+ (typeof frame.text === 'string' && frame.text.trim())
304
+ || typeof frame.prompt === 'string'
305
+ || Array.isArray(frame.prompt)
306
+ )) {
303
307
  // Optional submission metadata (additive, old viewers omit it): the
304
308
  // originating surface's submission id must survive the pipe so its
305
309
  // optimistic user row releases when the SAME id settles in the
306
310
  // owner's transcript (user: 방금 친 메세지가 2개 남는다).
307
311
  const id = typeof frame.id === 'string' && frame.id.trim() ? frame.id : undefined;
308
312
  const submittedAt = Number(frame.submittedAt);
309
- const delivered = onRemoteSubmit(frame.text, {
313
+ const prompt = frame.prompt ?? frame.text;
314
+ const delivered = onRemoteSubmit(prompt, {
315
+ ...(frame.options && typeof frame.options === 'object' ? frame.options : {}),
310
316
  ...(id ? { id } : {}),
311
317
  ...(Number.isFinite(submittedAt) && submittedAt > 0
312
318
  ? { submittedAt: Math.round(submittedAt) }
@@ -660,16 +666,21 @@ export function createLiveShare({
660
666
  ensure: ensureShare,
661
667
  viewerConnected: () => clientUp,
662
668
  waitForViewerSync,
663
- sendSubmit(text, meta = null) {
669
+ sendSubmit(prompt, meta = null) {
664
670
  if (!clientUp || !client) return false;
665
671
  const ackId = `ack-${process.pid}-${Date.now()}-${(submitAckSeq += 1)}`;
666
672
  const onUndelivered = typeof meta?.onUndelivered === 'function' ? meta.onUndelivered : null;
667
673
  try {
668
674
  const id = meta && meta.id != null && String(meta.id).trim() ? String(meta.id) : undefined;
669
675
  const submittedAt = Number(meta?.submittedAt);
676
+ const displayText = String(meta?.displayText
677
+ ?? meta?.options?.displayText
678
+ ?? (typeof prompt === 'string' ? prompt : ''));
670
679
  client.write(frameLine({
671
680
  t: 'submit',
672
- text: String(text),
681
+ text: displayText,
682
+ prompt,
683
+ ...(meta?.options && typeof meta.options === 'object' ? { options: meta.options } : {}),
673
684
  ack: ackId,
674
685
  ...(id ? { id } : {}),
675
686
  ...(Number.isFinite(submittedAt) && submittedAt > 0
@@ -723,9 +734,10 @@ export function createLiveShare({
723
734
  * the SAME identity and the owner reuses it for the settled user row — which is
724
735
  * what releases the submitting surface's optimistic bubble.
725
736
  */
726
- export function forwardViewerSubmit({ text, options = {}, share, spool, pid = process.pid }) {
737
+ export function forwardViewerSubmit({ prompt = null, text, options = {}, share, spool, pid = process.pid }) {
727
738
  const value = String(text || '').trim();
728
- if (!value) return false;
739
+ const content = prompt ?? value;
740
+ if (!value && !(Array.isArray(content) && content.length > 0)) return false;
729
741
  // Reconcile first so a session that became attachable this event-loop turn
730
742
  // takes the instant pipe path instead of the durable detour.
731
743
  try { share.ensure?.(); } catch { /* durable fallback below */ }
@@ -736,9 +748,11 @@ export function forwardViewerSubmit({ text, options = {}, share, spool, pid = pr
736
748
  // A pipe WRITE is not a delivery: the owner may refuse the prompt and the
737
749
  // socket may die between write and read, so an unacknowledged submit is
738
750
  // re-delivered through the spool — late, never lost.
739
- if (share.sendSubmit(value, {
751
+ if (share.sendSubmit(content, {
740
752
  id: submissionId,
741
753
  submittedAt: options.submittedAt,
754
+ displayText: value,
755
+ options,
742
756
  onUndelivered: deliverDurably,
743
757
  })) return true;
744
758
  return deliverDurably();
@@ -9,6 +9,7 @@ import {
9
9
  parseAgentJob,
10
10
  parseSyntheticAgentMessage,
11
11
  } from './agent-envelope.mjs';
12
+ import { attachmentTextForPart, isAttachmentReference } from '../../runtime/attachments/store.mjs';
12
13
 
13
14
  const QUEUE_PRIORITY = { now: 0, next: 1, later: 2 };
14
15
 
@@ -79,7 +80,7 @@ export function promptContentText(content) {
79
80
  if (Array.isArray(content)) {
80
81
  return content.map((part) => {
81
82
  if (typeof part === 'string') return part;
82
- if (part?.type === 'text') return part.text || '';
83
+ if (part?.type === 'text') return isAttachmentReference(part) ? attachmentTextForPart(part) : (part.text || '');
83
84
  if (part?.type === 'image') return '[Image]';
84
85
  if (part?.type === 'file') return '[File]';
85
86
  return part?.text || '';
@@ -151,10 +152,9 @@ export function mergePastedImages(entries) {
151
152
  return Object.keys(out).length > 0 ? out : null;
152
153
  }
153
154
 
154
- // Byte-free image metadata for the user transcript item. The prompt content
155
- // keeps the full base64 payload for the provider send; transcript items (and
156
- // therefore desktop snapshots published on every update) carry only
157
- // name/mime/size so previews can render without bloating state.
155
+ // Byte-free image metadata for the user transcript item. Prompt content carries
156
+ // only a content-addressed attachment ref after daemon intake; transcript items
157
+ // and desktop snapshots carry just name/mime/size.
158
158
  export function promptContentImageMeta(content, pastedImages) {
159
159
  const parts = Array.isArray(content) ? content.filter((part) => part?.type === 'image') : [];
160
160
  if (parts.length === 0) return null;
@@ -168,7 +168,7 @@ export function promptContentImageMeta(content, pastedImages) {
168
168
  id: meta?.id ?? null,
169
169
  name: String(meta?.filename || `Image ${index + 1}`),
170
170
  mimeType: String(part.mimeType || part.mediaType || meta?.mediaType || 'image/png'),
171
- bytes: data.length,
171
+ bytes: Number(part.sizeBytes) || data.length,
172
172
  };
173
173
  });
174
174
  }
@@ -584,9 +584,8 @@ export function createSessionApiB(bag) {
584
584
  }
585
585
  },
586
586
  // Desktop composer image attach: run the SAME optional-sharp resize
587
- // pipeline the TUI paste path uses (<=2000x2000, 5MB-base64 token budget,
588
- // identical no-sharp size errors) so both clients send identical image
589
- // payloads to the provider.
587
+ // pipeline the TUI paste path uses. The current provider selects the
588
+ // Anthropic 2000px/5MB profile or OpenAI 2048px/1536-patch profile.
590
589
  resizeImage: async ({ data, mimeType = 'image/png', filename = '' } = {}) => {
591
590
  const base64 = String(data || '');
592
591
  if (!base64) throw new Error('resizeImage: image payload is required');
@@ -595,7 +594,10 @@ export function createSessionApiB(bag) {
595
594
  const attachment = await imageAttachmentFromBuffer(
596
595
  Buffer.from(base64, 'base64'),
597
596
  String(mimeType || 'image/png'),
598
- { filename: String(filename || 'Pasted image') },
597
+ {
598
+ filename: String(filename || 'Pasted image'),
599
+ provider: routeState().provider || '',
600
+ },
599
601
  );
600
602
  return {
601
603
  data: attachment.content,
@@ -647,6 +649,12 @@ export function createSessionApiB(bag) {
647
649
  getTurnReviewDiff: async (options = {}) => {
648
650
  return (await runtime.getTurnReviewDiff?.(options)) ?? { supported: false, files: [], patch: '' };
649
651
  },
652
+ revertTurnReview: async () => {
653
+ if (typeof runtime.revertTurnReview !== 'function') {
654
+ throw new Error('Turn review revert is unavailable');
655
+ }
656
+ return await runtime.revertTurnReview();
657
+ },
650
658
  revertTurnReviewFile: async (file) => {
651
659
  if (typeof runtime.revertTurnReviewFile !== 'function') {
652
660
  throw new Error('Turn review revert is unavailable');
@@ -8,6 +8,7 @@ import { createSessionApiB } from './session-api-ext.mjs';
8
8
  import { buildDoctorReport } from '../app/doctor.mjs';
9
9
  import { recomputePromptHistory } from './prompt-history.mjs';
10
10
  import { appendPromptHistory, buildMergedPromptHistory, loadPromptHistory, promptHistoryKey } from '../prompt-history-store.mjs';
11
+ import { hydratePastedAttachments } from '../../runtime/attachments/store.mjs';
11
12
 
12
13
  export function createSessionApi(bag) {
13
14
  const api = { ...createSessionApiA(bag), ...createSessionApiB(bag) };
@@ -736,7 +737,15 @@ export function createSessionApiA(bag) {
736
737
  } catch { /* best-effort */ }
737
738
  }, 150);
738
739
  pendingAfterAbortKick.unref?.();
739
- return { aborted, restoreText, pastedImages: restorePastedImages, discardPastedImages, pastedTexts: restorePastedTexts, discardPastedTexts };
740
+ const restored = hydratePastedAttachments(restorePastedImages, restorePastedTexts);
741
+ return {
742
+ aborted,
743
+ restoreText,
744
+ pastedImages: restored.pastedImages,
745
+ discardPastedImages,
746
+ pastedTexts: restored.pastedTexts,
747
+ discardPastedTexts,
748
+ };
740
749
  },
741
750
  };
742
751
  }
@@ -6,6 +6,7 @@ import { resetAllStreamingMarkdownStablePrefixes } from '../markdown/streaming-m
6
6
  import { createSessionStats } from './session-stats.mjs';
7
7
  import { queuePriorityValue, defaultQueuePriority, isQueuedEntryEditable, isQueuedEntryVisible, isSlashQueuedEntry, notificationDisplayText, sessionActivityTimestamp, promptDisplayText, promptContentImageMeta, mergePromptContents, mergePastedImages, mergePastedTexts, callCommitCallbacks, STEERING_SUPPRESSED_DISPLAY } from './queue-helpers.mjs';
8
8
  import { appendTuiSteeringPersist, dropTuiSteeringPersist, drainTuiSteeringPersist } from './tui-steering-persist.mjs';
9
+ import { hydratePastedAttachments } from '../../runtime/attachments/store.mjs';
9
10
 
10
11
  export function createSessionFlow(bag) {
11
12
  const {
@@ -560,12 +561,13 @@ export function createSessionFlow(bag) {
560
561
  removeQueuedEntries(queued);
561
562
  const queuedText = queued.map((item) => item.text).filter((text) => String(text || '').trim()).join('\n');
562
563
  const combinedText = [queuedText, String(currentText || '')].filter((text) => text.trim()).join('\n');
564
+ const hydrated = hydratePastedAttachments(mergePastedImages(queued), mergePastedTexts(queued));
563
565
  return {
564
566
  count: queued.length,
565
567
  ids: queued.map((item) => String(item.id || '')).filter(Boolean),
566
568
  text: combinedText,
567
- pastedImages: mergePastedImages(queued),
568
- pastedTexts: mergePastedTexts(queued),
569
+ pastedImages: hydrated.pastedImages,
570
+ pastedTexts: hydrated.pastedTexts,
569
571
  };
570
572
  }
571
573
 
@@ -111,8 +111,12 @@ import { createSessionApi } from './session/session-api.mjs';
111
111
  import { createFrameBatchedStorePublisher } from './session/frame-batched-store.mjs';
112
112
  import { createLiveShare, forwardViewerSubmit, liveSharePipePath } from './session/live-share.mjs';
113
113
  import { displayModelName } from '../ui/model-display.mjs';
114
+ import { prewarmImageResizer } from '../runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs';
114
115
 
115
116
  const SESSION_RUNTIME_MODULE = '../mixdog-session-runtime.mjs';
117
+ // Start the native binding load with daemon startup rather than making the
118
+ // user's first image attachment pay the cold dynamic-import cost.
119
+ void prewarmImageResizer();
116
120
 
117
121
  // The runtime graph is imported lazily, but that import (measured ~250ms) used
118
122
  // to land inside the FIRST session runtime creation, which desktop performs while it
@@ -869,9 +873,13 @@ export async function createLocalSessionRuntime({
869
873
  if (injected.length === 0) return;
870
874
  for (const item of injected) {
871
875
  if (!item || typeof item !== 'object' || (item.text == null && item.content == null)) continue;
872
- const text = item.text ?? item.content;
876
+ const content = item.content ?? item.text;
873
877
  const id = typeof item.id === 'string' && item.id.trim() ? item.id.trim() : undefined;
874
- bag.enqueue(text, id ? { id } : {});
878
+ bag.enqueue(content, {
879
+ ...(item.options && typeof item.options === 'object' ? item.options : {}),
880
+ ...(item.text ? { displayText: item.text } : {}),
881
+ ...(id ? { id } : {}),
882
+ });
875
883
  }
876
884
  void bag.drain();
877
885
  })().catch(() => { /* the watch/tick pair retries */ });
@@ -889,7 +897,7 @@ export async function createLocalSessionRuntime({
889
897
  socketPathFor: (id) => liveSharePipePath(id, sessionPath(id)),
890
898
  getPublishedState: () => publishedState,
891
899
  listeners,
892
- onRemoteSubmit: (text, meta) => {
900
+ onRemoteSubmit: (prompt, meta) => {
893
901
  // A refusal must be REPORTED, never silent. This session runtime can be unable to
894
902
  // take a foreign prompt (disposed, or it became an attached viewer
895
903
  // itself); swallowing it here is what made a submitted message vanish
@@ -900,8 +908,8 @@ export async function createLocalSessionRuntime({
900
908
  // the settled user item then carry the id the submitting surface used
901
909
  // for its optimistic row, so that row releases instead of duplicating.
902
910
  const queued = bag.enqueue(
903
- text,
904
- meta && meta.id ? { id: meta.id, submittedAt: meta.submittedAt } : {},
911
+ prompt,
912
+ meta && typeof meta === 'object' ? meta : {},
905
913
  ) !== false;
906
914
  void bag.drain();
907
915
  return queued;
@@ -951,6 +959,7 @@ export async function createLocalSessionRuntime({
951
959
  if (!text) return { accepted: false };
952
960
  return {
953
961
  accepted: forwardViewerSubmit({
962
+ prompt,
954
963
  text,
955
964
  options,
956
965
  share: liveShare,
@@ -960,6 +969,7 @@ export async function createLocalSessionRuntime({
960
969
  content: prompt,
961
970
  text,
962
971
  id: submissionId,
972
+ options,
963
973
  }) === true,
964
974
  }),
965
975
  };