oh-my-knowledge 0.52.2 → 0.52.3

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.
@@ -15,7 +15,7 @@ export interface CodexIndexedTask {
15
15
  endLine: number;
16
16
  }
17
17
  export interface CodexRolloutIndex {
18
- schemaVersion: 11;
18
+ schemaVersion: 12;
19
19
  sourcePath: string;
20
20
  /** File size observed when the index was produced. */
21
21
  sourceSize: number;
@@ -3,7 +3,7 @@ import { codexUserDisplayText, codexUserMessageOrigin } from './codex-protocol.j
3
3
  import { codexRuntimeToolOutcomeFromPayload, codexToolOutputOutcome, } from './codex-tool-status.js';
4
4
  const READ_CHUNK_BYTES = 256 * 1024;
5
5
  const MAX_RECORD_BYTES = 32 * 1024 * 1024;
6
- const INDEX_SCHEMA_VERSION = 11;
6
+ const INDEX_SCHEMA_VERSION = 12;
7
7
  const MAX_CURRENT_INDEX_ATTEMPTS = 4;
8
8
  /**
9
9
  * Build a compact byte-range index without retaining the rollout. Only records
@@ -1,8 +1,14 @@
1
1
  /** Stable inventory of Codex rollout record shapes understood by OMK. */
2
+ export interface CodexUserAttachment {
3
+ attachmentKind: 'image' | 'file';
4
+ name: string;
5
+ }
2
6
  export declare function isCodexResponseItemType(value: string | undefined): boolean;
3
7
  export declare function isCodexEventMessageType(value: string | undefined): boolean;
4
8
  /** Preserve source text separately while removing Codex UI transport envelopes from semantic display. */
5
9
  export declare function codexUserDisplayText(text: string): string | undefined;
10
+ /** Extract privacy-safe attachment metadata without projecting local paths into Trace IR. */
11
+ export declare function codexUserAttachments(text: string): CodexUserAttachment[];
6
12
  export declare function codexUserMessageOrigin(text: string): 'human' | 'runtime';
7
13
  /**
8
14
  * Records consumed by correlation or retained only as source provenance do not
@@ -32,6 +32,9 @@ const EVENT_MESSAGE_TYPES = new Set([
32
32
  ]);
33
33
  const IN_APP_BROWSER_CONTEXT_RE = /<in-app-browser-context\b[^>]*>[\s\S]*?<\/in-app-browser-context>/gi;
34
34
  const CODEX_RUNTIME_MESSAGE_RE = /^# AGENTS\.md instructions\b|^<(?:app-context|environment_context|permissions instructions|collaboration_mode|apps_instructions|plugins_instructions|skills_instructions|recommended_plugins)>/i;
35
+ const CODEX_USER_REQUEST_HEADING_RE = /^## My request(?: for Codex)?:\s*$/im;
36
+ const CODEX_USER_FILES_HEADING_RE = /^# Files mentioned by the user:\s*$/im;
37
+ const IMAGE_ATTACHMENT_RE = /\.(?:avif|bmp|gif|heic|heif|jpe?g|png|webp)$/i;
35
38
  export function isCodexResponseItemType(value) {
36
39
  return value !== undefined && RESPONSE_ITEM_TYPES.has(value);
37
40
  }
@@ -40,8 +43,7 @@ export function isCodexEventMessageType(value) {
40
43
  }
41
44
  /** Preserve source text separately while removing Codex UI transport envelopes from semantic display. */
42
45
  export function codexUserDisplayText(text) {
43
- const requestHeading = /^## My request for Codex:\s*$/im;
44
- const requestMatch = requestHeading.exec(text);
46
+ const requestMatch = CODEX_USER_REQUEST_HEADING_RE.exec(text);
45
47
  const request = requestMatch
46
48
  ? text.slice(requestMatch.index + requestMatch[0].length)
47
49
  : text;
@@ -53,6 +55,39 @@ export function codexUserDisplayText(text) {
53
55
  .trim();
54
56
  return visible || undefined;
55
57
  }
58
+ /** Extract privacy-safe attachment metadata without projecting local paths into Trace IR. */
59
+ export function codexUserAttachments(text) {
60
+ const attachments = new Map();
61
+ const requestMatch = CODEX_USER_REQUEST_HEADING_RE.exec(text);
62
+ const filesMatch = CODEX_USER_FILES_HEADING_RE.exec(text);
63
+ if (filesMatch && (!requestMatch || filesMatch.index < requestMatch.index)) {
64
+ const sectionEnd = requestMatch?.index ?? text.length;
65
+ const section = text.slice(filesMatch.index + filesMatch[0].length, sectionEnd);
66
+ for (const match of section.matchAll(/^##\s+(.+?):\s+(.+)\s*$/gm)) {
67
+ addCodexUserAttachment(attachments, match[1], match[2]);
68
+ }
69
+ }
70
+ for (const match of text.matchAll(/<image\b[^>]*\bpath="([^"]+)"[^>]*>/gi)) {
71
+ addCodexUserAttachment(attachments, undefined, match[1]);
72
+ }
73
+ return [...attachments.values()];
74
+ }
75
+ function addCodexUserAttachment(attachments, declaredName, sourcePath) {
76
+ const normalizedPath = sourcePath?.trim();
77
+ const inferredName = normalizedPath?.split(/[\\/]/).at(-1);
78
+ const name = declaredName?.trim() || inferredName?.trim();
79
+ if (!name)
80
+ return;
81
+ const key = name.toLocaleLowerCase('en-US');
82
+ if (attachments.has(key))
83
+ return;
84
+ attachments.set(key, {
85
+ attachmentKind: IMAGE_ATTACHMENT_RE.test(name) || IMAGE_ATTACHMENT_RE.test(normalizedPath ?? '')
86
+ ? 'image'
87
+ : 'file',
88
+ name,
89
+ });
90
+ }
56
91
  export function codexUserMessageOrigin(text) {
57
92
  const trimmed = text.trimStart();
58
93
  if (CODEX_RUNTIME_MESSAGE_RE.test(trimmed))
@@ -4,7 +4,7 @@ import { correlateTraceToolEvents, createTraceId, normalizeTraceTimestamp, trace
4
4
  import { nonNegativeMetric, optionalTokenCount, splitInclusiveInputTokens, tokenCount, } from '../shared/token-usage.js';
5
5
  import { normalizeToolIdentity } from '../shared/tool-identity.js';
6
6
  import { extractCodexExecCommands } from './codex-exec-command.js';
7
- import { codexUserDisplayText, codexUserMessageOrigin, isCodexEventMessageType, isCodexRecordConsumedWithoutDirectEvent, isCodexResponseItemType, } from './codex-protocol.js';
7
+ import { codexUserAttachments, codexUserDisplayText, codexUserMessageOrigin, isCodexEventMessageType, isCodexRecordConsumedWithoutDirectEvent, isCodexResponseItemType, } from './codex-protocol.js';
8
8
  import { codexRuntimeToolOutcome, codexToolOutputOutcome, codexToolStatusFromValue, } from './codex-tool-status.js';
9
9
  export function isCodexJsonl(records) {
10
10
  return records.some((record) => {
@@ -231,6 +231,7 @@ function convertCodexRecords(rawRecords, runId) {
231
231
  : normalizedRole === 'system' ? 'runtime' : 'synthetic',
232
232
  text,
233
233
  displayText: normalizedRole === 'user' ? codexUserDisplayText(text) : undefined,
234
+ attachments: normalizedRole === 'user' ? codexUserAttachments(text) : undefined,
234
235
  model: normalizedRole === 'assistant' ? activeModel : undefined,
235
236
  });
236
237
  }
@@ -444,6 +445,7 @@ function convertCodexRecords(rawRecords, runId) {
444
445
  origin: codexUserMessageOrigin(text),
445
446
  text,
446
447
  displayText: codexUserDisplayText(text),
448
+ attachments: codexUserAttachments(text),
447
449
  });
448
450
  }
449
451
  return;
@@ -995,6 +997,7 @@ function indexDuplicateEventMessages(records) {
995
997
  const text = stringValue(payload.message);
996
998
  if (!role || !text)
997
999
  return;
1000
+ const mirrorText = role === 'user' ? codexUserDisplayText(text) : text;
998
1001
  const nearbyRecords = [-1, 1].flatMap((direction) => {
999
1002
  let candidateIndex = sourceIndex + direction;
1000
1003
  while (candidateIndex >= 0 && candidateIndex < records.length) {
@@ -1010,9 +1013,14 @@ function indexDuplicateEventMessages(records) {
1010
1013
  if (adjacent?.type !== 'response_item')
1011
1014
  return false;
1012
1015
  const adjacentPayload = isObject(adjacent.payload) ? adjacent.payload : {};
1013
- return adjacentPayload.type === 'message'
1014
- && adjacentPayload.role === role
1015
- && codexContentText(adjacentPayload.content) === text;
1016
+ if (adjacentPayload.type !== 'message' || adjacentPayload.role !== role)
1017
+ return false;
1018
+ const adjacentText = codexContentText(adjacentPayload.content);
1019
+ if (!adjacentText)
1020
+ return false;
1021
+ return role === 'user'
1022
+ ? Boolean(mirrorText) && codexUserDisplayText(adjacentText) === mirrorText
1023
+ : adjacentText === text;
1016
1024
  });
1017
1025
  if (mirrored)
1018
1026
  duplicateIndexes.add(sourceIndex);
@@ -983,13 +983,20 @@ function isTimelineEvent(value) {
983
983
  ];
984
984
  if (!optionalStrings.every((field) => field === undefined || typeof field === 'string')
985
985
  || !isOptionalTimestamp(value.timestamp)
986
- || (value.isError !== undefined && typeof value.isError !== 'boolean'))
986
+ || (value.isError !== undefined && typeof value.isError !== 'boolean')
987
+ || (value.attachments !== undefined && !isTimelineAttachmentArray(value.attachments)))
987
988
  return false;
988
989
  return value.kind !== 'tool_result'
989
990
  || value.toolStatus === undefined
990
991
  || value.isError === undefined
991
992
  || value.isError === (value.toolStatus === 'failure');
992
993
  }
994
+ function isTimelineAttachmentArray(value) {
995
+ return Array.isArray(value) && value.every((attachment) => (isObjectRecord(attachment)
996
+ && (attachment.attachmentKind === 'image' || attachment.attachmentKind === 'file')
997
+ && typeof attachment.name === 'string'
998
+ && attachment.name.length > 0));
999
+ }
993
1000
  function isTimelineEventArray(value) {
994
1001
  return Array.isArray(value) && value.every(isTimelineEvent);
995
1002
  }
@@ -2746,6 +2753,7 @@ function userTimelineEvents(event, base, order) {
2746
2753
  order: order + (commandEnvelope ? 1 : 0),
2747
2754
  snippet: snippet(semanticText, 700),
2748
2755
  fullText: fullText(semanticText),
2756
+ attachments: event.attachments,
2749
2757
  label: userTextEventLabel(kind),
2750
2758
  }));
2751
2759
  return events;
@@ -12,6 +12,11 @@ export type TraceModelActivityVisibility = 'plaintext' | 'opaque';
12
12
  export type TraceModelActivityContentSource = 'summary' | 'content' | 'text';
13
13
  export type TraceRuntimeContextKind = 'session_context' | 'execution_context' | 'settings' | 'goal';
14
14
  export type TraceAgentActivityKind = 'communication' | 'status';
15
+ export interface TraceMessageAttachment {
16
+ attachmentKind: 'image' | 'file';
17
+ /** Privacy-safe display name. Source-local paths remain available only in raw logs. */
18
+ name: string;
19
+ }
15
20
  /** Source-neutral tool identity, including provider namespaces when present. */
16
21
  export type TraceToolRef = NormalizedToolIdentity;
17
22
  interface TraceEventBase {
@@ -30,6 +35,7 @@ export interface TraceMessageEvent extends TraceEventBase {
30
35
  text: string;
31
36
  /** Human-facing text after a source adapter removes transport/UI envelopes. */
32
37
  displayText?: string;
38
+ attachments?: TraceMessageAttachment[];
33
39
  model?: string;
34
40
  attributionSkill?: string;
35
41
  }
@@ -4,6 +4,13 @@ export interface ConversationActivitySnapshot {
4
4
  revision: string;
5
5
  runningCount: number;
6
6
  }
7
+ export interface ConversationDetailActivitySnapshot {
8
+ schemaVersion: 1;
9
+ revision: string;
10
+ taskCount: number;
11
+ runningCount: number;
12
+ }
7
13
  export declare function buildConversationActivitySnapshot(model: ConversationIndexViewModel): ConversationActivitySnapshot;
14
+ export declare function buildConversationDetailActivitySnapshot(conversation: ConversationListItem): ConversationDetailActivitySnapshot;
8
15
  export declare function renderConversationIndexPage(model: ConversationIndexViewModel, lang?: Lang): string;
9
16
  export declare function renderConversationDetailPage(conversation: ConversationListItem, lang?: Lang): string;
@@ -21,6 +21,20 @@ export function buildConversationActivitySnapshot(model) {
21
21
  runningCount,
22
22
  };
23
23
  }
24
+ export function buildConversationDetailActivitySnapshot(conversation) {
25
+ const state = conversation.tasks.map((task) => [
26
+ task.turnId,
27
+ task.status,
28
+ task.startTimestamp ?? null,
29
+ task.endTimestamp ?? null,
30
+ ]);
31
+ return {
32
+ schemaVersion: 1,
33
+ revision: createHash('sha256').update(JSON.stringify(state)).digest('hex').slice(0, 24),
34
+ taskCount: conversation.tasks.length,
35
+ runningCount: conversation.tasks.filter((task) => task.status === 'open').length,
36
+ };
37
+ }
24
38
  function latestOpenConversationTask(conversation) {
25
39
  for (let index = conversation.tasks.length - 1; index >= 0; index -= 1) {
26
40
  const task = conversation.tasks[index];
@@ -94,11 +108,12 @@ export function renderConversationIndexPage(model, lang = DEFAULT_LANG) {
94
108
  export function renderConversationDetailPage(conversation, lang = DEFAULT_LANG) {
95
109
  const zh = lang === 'zh';
96
110
  const langSuffix = lang === DEFAULT_LANG ? '' : '?lang=en';
111
+ const activity = buildConversationDetailActivitySnapshot(conversation);
97
112
  const taskRows = conversationTaskEntries(conversation.tasks)
98
113
  .map(({ task, ordinal }) => renderTaskRow(task, ordinal, lang))
99
114
  .join('');
100
115
  return layout(zh ? '对话任务' : 'Conversation tasks', `
101
- <main class="conversation-page conversation-detail-page">
116
+ <main class="conversation-page conversation-detail-page" data-activity-revision="${e(activity.revision)}" data-activity-endpoint="/api/conversations/${encodeURIComponent(conversation.threadId)}/activity${langSuffix}">
102
117
  <header class="conversation-page-head conversation-detail-head">
103
118
  <div>
104
119
  <a class="back-link" href="/conversations${langSuffix}">${zh ? '返回对话总览' : 'Back to conversations'}</a>
@@ -114,11 +129,12 @@ export function renderConversationDetailPage(conversation, lang = DEFAULT_LANG)
114
129
  </div>
115
130
  </header>
116
131
  <section class="task-list" aria-label="${zh ? '任务列表' : 'Task list'}">
117
- <header class="task-list-head"><span>${zh ? '任务' : 'Task'}</span><span>${zh ? '时间' : 'Time'}</span><span>${zh ? '执行' : 'Execution'}</span><span></span></header>
132
+ <header class="task-list-head"><span>${zh ? '任务' : 'Task'}</span><span>${zh ? '时间' : 'Time'}</span><span>${zh ? '执行' : 'Execution'}</span><button type="button" class="task-order-toggle" data-task-order-toggle data-task-order="desc" aria-label="${zh ? '切换为最早优先' : 'Switch to oldest first'}" title="${zh ? '切换为最早优先' : 'Switch to oldest first'}">${icon('arrow-up-down', { size: 14 })}<span data-task-order-label>${zh ? '最新优先' : 'Newest first'}</span></button></header>
118
133
  ${taskRows || `<div class="empty-state"><strong>${zh ? '没有识别到任务边界' : 'No task boundaries found'}</strong><span>${zh ? '该对话的原始日志可能尚未写入完整的 turn 生命周期。' : 'The raw log may not contain complete turn lifecycle records yet.'}</span></div>`}
119
134
  </section>
120
135
  </main>
121
136
  <style>${CSS}</style>
137
+ <script>${conversationDetailScript(lang)}</script>
122
138
  `, lang);
123
139
  }
124
140
  function renderConversationRow(conversation, lang) {
@@ -161,14 +177,13 @@ function renderConversationRow(conversation, lang) {
161
177
  function conversationTaskEntries(tasks) {
162
178
  return tasks
163
179
  .map((task, index) => ({ task, ordinal: index + 1 }))
164
- .sort((left, right) => (Number(right.task.status === 'open') - Number(left.task.status === 'open')
165
- || left.ordinal - right.ordinal));
180
+ .sort((left, right) => right.ordinal - left.ordinal);
166
181
  }
167
182
  function renderTaskRow(task, ordinal, lang) {
168
183
  const zh = lang === 'zh';
169
184
  const href = taskTrajectoryHref(task, lang) ?? '#';
170
185
  const status = statusLabel(task.status, lang);
171
- return `<article class="task-row${task.status === 'open' ? ' is-running' : ''}" data-task-status="${e(task.status)}">
186
+ return `<article class="task-row${task.status === 'open' ? ' is-running' : ''}" data-task-status="${e(task.status)}" data-task-ordinal="${ordinal}">
172
187
  <div class="task-index">${String(ordinal).padStart(2, '0')}</div>
173
188
  <div class="task-main"><div class="task-title">${renderSafeInlineMarkdown(task.title, { links: 'text' })}</div><div class="task-context">${task.eventCount} ${zh ? '条原始日志' : 'raw records'}</div></div>
174
189
  <div class="task-time"><span>${e(formatTime(task.startTimestamp))}</span><small>${e(formatDuration(task.durationMs, lang))}</small></div>
@@ -176,6 +191,93 @@ function renderTaskRow(task, ordinal, lang) {
176
191
  <a class="trajectory-link" href="${e(href)}">${zh ? '查看任务轨迹' : 'View trajectory'} →</a>
177
192
  </article>`;
178
193
  }
194
+ function conversationDetailScript(lang) {
195
+ const newestLabel = lang === 'zh' ? '最新优先' : 'Newest first';
196
+ const oldestLabel = lang === 'zh' ? '最早优先' : 'Oldest first';
197
+ const switchToNewest = lang === 'zh' ? '切换为最新优先' : 'Switch to newest first';
198
+ const switchToOldest = lang === 'zh' ? '切换为最早优先' : 'Switch to oldest first';
199
+ return `
200
+ (() => {
201
+ const root = document.querySelector('.conversation-detail-page');
202
+ const taskList = root?.querySelector('.task-list');
203
+ const toggle = root?.querySelector('[data-task-order-toggle]');
204
+ const label = toggle?.querySelector('[data-task-order-label]');
205
+ const rows = [...(taskList?.querySelectorAll('.task-row') || [])];
206
+ const preferenceKey = 'omk.conversationTaskOrder';
207
+ let activityTimer;
208
+ let activityRequest;
209
+ let activityStopped = false;
210
+
211
+ const applyOrder = (order) => {
212
+ if (!taskList || !toggle) return;
213
+ const sorted = [...rows].sort((left, right) => {
214
+ const leftOrdinal = Number(left.dataset.taskOrdinal || 0);
215
+ const rightOrdinal = Number(right.dataset.taskOrdinal || 0);
216
+ return order === 'asc' ? leftOrdinal - rightOrdinal : rightOrdinal - leftOrdinal;
217
+ });
218
+ for (const row of sorted) taskList.append(row);
219
+ toggle.dataset.taskOrder = order;
220
+ if (label) label.textContent = order === 'asc' ? '${oldestLabel}' : '${newestLabel}';
221
+ const actionLabel = order === 'asc' ? '${switchToNewest}' : '${switchToOldest}';
222
+ toggle.setAttribute('aria-label', actionLabel);
223
+ toggle.setAttribute('title', actionLabel);
224
+ };
225
+
226
+ let initialOrder = 'desc';
227
+ try {
228
+ const storedOrder = sessionStorage.getItem(preferenceKey);
229
+ if (storedOrder === 'asc' || storedOrder === 'desc') initialOrder = storedOrder;
230
+ } catch { /* Storage can be unavailable in hardened browser contexts. */ }
231
+ applyOrder(initialOrder);
232
+ toggle?.addEventListener('click', () => {
233
+ const order = toggle.dataset.taskOrder === 'asc' ? 'desc' : 'asc';
234
+ applyOrder(order);
235
+ try { sessionStorage.setItem(preferenceKey, order); } catch { /* Ignore unavailable storage. */ }
236
+ });
237
+
238
+ const stopActivityPolling = () => {
239
+ activityStopped = true;
240
+ if (activityTimer) clearTimeout(activityTimer);
241
+ activityRequest?.abort();
242
+ };
243
+ const scheduleActivityPoll = (delay = 5000) => {
244
+ if (activityStopped) return;
245
+ if (activityTimer) clearTimeout(activityTimer);
246
+ activityTimer = setTimeout(pollActivity, delay);
247
+ };
248
+ const pollActivity = async () => {
249
+ if (activityStopped) return;
250
+ if (document.hidden) {
251
+ scheduleActivityPoll();
252
+ return;
253
+ }
254
+ const endpoint = root?.dataset.activityEndpoint;
255
+ if (!endpoint) return;
256
+ activityRequest?.abort();
257
+ activityRequest = new AbortController();
258
+ try {
259
+ const response = await fetch(endpoint, {
260
+ cache: 'no-store',
261
+ signal: activityRequest.signal,
262
+ });
263
+ if (!response.ok) throw new Error('conversation activity unavailable');
264
+ const snapshot = await response.json();
265
+ if (snapshot.revision && snapshot.revision !== root?.dataset.activityRevision) {
266
+ window.location.reload();
267
+ return;
268
+ }
269
+ } catch (cause) {
270
+ if (cause?.name === 'AbortError') return;
271
+ }
272
+ scheduleActivityPoll();
273
+ };
274
+ document.addEventListener('visibilitychange', () => {
275
+ if (!document.hidden) scheduleActivityPoll(0);
276
+ });
277
+ window.addEventListener('pagehide', stopActivityPolling, { once: true });
278
+ scheduleActivityPoll();
279
+ })();`;
280
+ }
179
281
  function taskTrajectoryHref(task, lang) {
180
282
  if (task.trajectoryHref)
181
283
  return withLang(task.trajectoryHref, lang);
@@ -391,7 +493,7 @@ const CSS = `
391
493
  .conversation-activity{display:flex;flex-direction:column;min-width:0}.conversation-activity strong{font-size:12px;font-weight:650;white-space:nowrap}.conversation-activity span{color:var(--text-muted);font:500 11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.running-label{display:flex;align-items:center;gap:6px;color:var(--accent)}.running-label i,.live-task-link i{width:6px;height:6px;border-radius:50%;background:var(--accent);box-shadow:0 0 0 3px rgba(79,70,229,.1)}
392
494
  .conversation-main{min-width:0}.conversation-title{display:block;font-size:14px;font-weight:650;line-height:1.35;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0}.conversation-main:has(.conversation-context) .conversation-title{margin-bottom:4px}.conversation-title code,.task-title code{padding:1px 3px;border-radius:3px;background:var(--bg-elevated);font:600 .92em ui-monospace,SFMono-Regular,Menlo,monospace}.inline-markdown-link{color:var(--accent);text-decoration:underline;text-decoration-color:rgba(79,70,229,.28);text-underline-offset:3px}.inline-markdown-link:hover{text-decoration-color:currentColor}.conversation-meta,.conversation-context,.detail-meta{display:flex;align-items:center;gap:8px;color:var(--text-muted);font-size:11px;min-width:0}.conversation-meta span+span:before,.conversation-context span+span:before,.detail-meta span+span:before{content:'·';margin-right:8px}.source-mark{color:var(--accent);font-weight:700}.conversation-workspace{min-width:0;color:var(--text-secondary);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
393
495
  .conversation-counts{display:flex;flex-direction:column;justify-content:center;align-items:flex-end;gap:2px;color:var(--text-secondary);font-size:11px;white-space:nowrap}.conversation-counts span{display:flex;gap:4px;align-items:baseline}.conversation-counts b{font-size:14px;color:var(--text-primary);font-variant-numeric:tabular-nums}.live-task-link{z-index:3!important;display:flex;align-items:center;gap:6px;color:var(--accent);font-weight:650;text-decoration:none;pointer-events:auto!important}.live-task-link:hover{text-decoration:underline}.failure-count{color:var(--red)!important}.index-pending{color:var(--text-muted)}.row-arrow{display:flex;align-items:center;justify-content:center;color:var(--text-faint);transition:color .14s,transform .14s}.conversation-row:hover .row-arrow{color:var(--accent);transform:translateX(2px)}.empty-state{display:flex;flex-direction:column;gap:4px;padding:36px;border:1px solid var(--border);border-radius:8px;background:var(--bg-surface);color:var(--text-secondary)}.empty-state strong{color:var(--text-primary)}
394
- .conversation-detail-head{align-items:flex-start}.conversation-detail-head h1{max-width:920px;font-size:24px}.detail-meta{margin-top:10px}.task-list-head,.task-row{display:grid;grid-template-columns:minmax(0,1fr) 140px 160px 150px;gap:18px;align-items:center}.task-list-head{padding:10px 22px 10px 72px;color:var(--text-muted);font-size:12px;background:var(--bg-elevated);border-bottom:1px solid var(--border)}.task-row{position:relative;padding:17px 22px 17px 72px;border-bottom:1px solid var(--border)}.task-row.is-running{background:rgba(79,70,229,.035);box-shadow:inset 3px 0 0 var(--accent)}.task-row:last-child{border-bottom:0}.task-index{position:absolute;left:22px;top:19px;font:700 12px ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-muted)}.task-row.is-running .task-index{color:var(--accent)}.task-main{min-width:0}.task-title{display:block;font-weight:650;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:6px}.task-context{color:var(--text-muted);font-size:12px}.task-time,.task-execution{display:flex;flex-direction:column;gap:3px}.task-time small,.task-execution small{color:var(--text-muted)}.task-status{font-weight:650}.status-aborted,.status-interrupted{color:var(--red)}.status-open{color:var(--accent)}.status-unknown{color:var(--yellow)}.status-completed{color:var(--green)}.trajectory-link{text-align:right;font-size:13px;white-space:nowrap}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
496
+ .conversation-detail-head{align-items:flex-start}.conversation-detail-head h1{max-width:920px;font-size:24px}.detail-meta{margin-top:10px}.task-list-head,.task-row{display:grid;grid-template-columns:minmax(0,1fr) 140px 160px 150px;gap:18px;align-items:center}.task-list-head{padding:8px 22px 8px 72px;color:var(--text-muted);font-size:12px;background:var(--bg-elevated);border-bottom:1px solid var(--border)}.task-order-toggle{justify-self:end;display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 8px;border:1px solid transparent;border-radius:5px;background:transparent;color:var(--text-secondary);font:inherit;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap}.task-order-toggle:hover{border-color:var(--border);background:var(--bg-surface);color:var(--text-primary)}.task-order-toggle:focus-visible{outline:2px solid rgba(79,70,229,.42);outline-offset:1px}.task-row{position:relative;padding:17px 22px 17px 72px;border-bottom:1px solid var(--border)}.task-row.is-running{background:rgba(79,70,229,.035);box-shadow:inset 3px 0 0 var(--accent)}.task-row:last-child{border-bottom:0}.task-index{position:absolute;left:22px;top:19px;font:700 12px ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-muted)}.task-row.is-running .task-index{color:var(--accent)}.task-main{min-width:0}.task-title{display:block;font-weight:650;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:6px}.task-context{color:var(--text-muted);font-size:12px}.task-time,.task-execution{display:flex;flex-direction:column;gap:3px}.task-time small,.task-execution small{color:var(--text-muted)}.task-status{font-weight:650}.status-aborted,.status-interrupted{color:var(--red)}.status-open{color:var(--accent)}.status-unknown{color:var(--yellow)}.status-completed{color:var(--green)}.trajectory-link{text-align:right;font-size:13px;white-space:nowrap}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
395
497
 
396
498
  html:has(.conversation-index-app),body:has(.conversation-index-app){height:100%;overflow:hidden;scrollbar-gutter:auto}
397
499
  body:has(.conversation-index-app) .app-bar{display:none}
@@ -16,6 +16,7 @@ const PATHS = {
16
16
  chip: '<rect x="4" y="4" width="16" height="16" rx="3"/><path d="M8 12h8"/>',
17
17
  'chevron-right': '<path d="M9 6l6 6-6 6"/>',
18
18
  'chevron-left': '<path d="M15 18l-6-6 6-6"/>',
19
+ 'arrow-up-down': '<path d="M7 20V4M3 8l4-4 4 4M17 4v16M13 16l4 4 4-4"/>',
19
20
  'zoom-in': '<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.4-4.4M11 8v6M8 11h6"/>',
20
21
  'zoom-out': '<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.4-4.4M8 11h6"/>',
21
22
  'maximize-2': '<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>',
@@ -1335,6 +1335,8 @@ function projectReplay(model, lang, options) {
1335
1335
  ? 'failure'
1336
1336
  : conversation ? 'message' : 'result';
1337
1337
  const opaqueModelActivity = modelActivity && event?.contentVisibility === 'opaque';
1338
+ const messageAttachments = event?.attachments ?? [];
1339
+ const messageAttachmentLabel = attachmentSummary(messageAttachments, lang);
1338
1340
  const text = eventPreview(event, opaqueModelActivity
1339
1341
  ? (zh ? '不可见' : 'Unavailable')
1340
1342
  : (zh ? '没有可展示的事件内容。' : 'No event content available.'));
@@ -1351,7 +1353,7 @@ function projectReplay(model, lang, options) {
1351
1353
  : STEP_LABELS[step.stepKind][lang],
1352
1354
  model: eventModel,
1353
1355
  title: text,
1354
- detail: '',
1356
+ detail: messageAttachmentLabel,
1355
1357
  facetIds: operationFacetIds,
1356
1358
  rawId: event ? `${event.kind} · ${event.id}` : step.id, primary: true,
1357
1359
  compact: opaqueModelActivity,
@@ -1393,6 +1395,11 @@ function projectReplay(model, lang, options) {
1393
1395
  { label: zh ? '角色' : 'Role', value: roleLabel(event, lang), detail: event?.kind ?? step.stepKind },
1394
1396
  { label: zh ? '时间' : 'Time', value: formatRelativeTimestamp(step.timestamp, startTimestamp), detail: formatDisplayTimestamp(step.timestamp, lang) },
1395
1397
  ...(eventModel ? [{ label: zh ? '模型' : 'Model', value: eventModel, detail: zh ? '由 trace 明确记录的事件模型' : 'Event model explicitly recorded by the trace' }] : []),
1398
+ ...(messageAttachments.length > 0 ? [{
1399
+ label: zh ? '附件' : 'Attachments',
1400
+ value: messageAttachmentLabel,
1401
+ detail: messageAttachments.map((attachment) => attachment.name).join('\n'),
1402
+ }] : []),
1396
1403
  {
1397
1404
  label: zh ? '内容' : 'Content',
1398
1405
  value: text,
@@ -1488,6 +1495,14 @@ function replayEventModel(step, event) {
1488
1495
  ? event?.model?.trim() || undefined
1489
1496
  : undefined;
1490
1497
  }
1498
+ function attachmentSummary(attachments, lang) {
1499
+ const imageCount = attachments.filter((attachment) => attachment.attachmentKind === 'image').length;
1500
+ const fileCount = attachments.length - imageCount;
1501
+ const parts = lang === 'zh'
1502
+ ? [imageCount > 0 ? `图片 ${imageCount} 张` : '', fileCount > 0 ? `文件 ${fileCount} 个` : '']
1503
+ : [imageCount > 0 ? `${imageCount} image${imageCount === 1 ? '' : 's'}` : '', fileCount > 0 ? `${fileCount} file${fileCount === 1 ? '' : 's'}` : ''];
1504
+ return parts.filter(Boolean).join(' · ');
1505
+ }
1491
1506
  function replayCardWidth(step, lang, pendingToolResults) {
1492
1507
  const event = step.events[0];
1493
1508
  if (step.stepKind === 'model_activity' && event?.contentVisibility === 'opaque')
@@ -9,7 +9,7 @@ import { renderDoctorDetail } from '../renderer/doctor-detail-renderer.js';
9
9
  import { assessHealth, renderSkillDetail } from '../renderer/skill-detail-renderer.js';
10
10
  import { renderObservationInboxPage } from '../renderer/observation-inbox-renderer.js';
11
11
  import { renderKnowledgeDebuggerPage } from '../renderer/knowledge-debugger-renderer.js';
12
- import { buildConversationActivitySnapshot, renderConversationDetailPage, renderConversationIndexPage, } from '../renderer/conversation-renderer.js';
12
+ import { buildConversationActivitySnapshot, buildConversationDetailActivitySnapshot, renderConversationDetailPage, renderConversationIndexPage, } from '../renderer/conversation-renderer.js';
13
13
  import { DEFAULT_LANG, e, t, layout } from '../renderer/layout.js';
14
14
  import { loadAllManagedRecords, resolveManagedDir, managedDir as projectManagedDir, listManagedRows, buildVersionScores } from '../managed/index.js';
15
15
  import { renderManagedList, renderManagedHistory } from '../renderer/managed-history-renderer.js';
@@ -901,6 +901,26 @@ export function createReportServer({ port, host: hostOption, reportsDir, analyse
901
901
  res.end(JSON.stringify(snapshot));
902
902
  return;
903
903
  }
904
+ const conversationActivityMatch = path.match(/^\/api\/conversations\/([^/]+)\/activity$/);
905
+ if (conversationActivityMatch) {
906
+ let threadId = '';
907
+ try {
908
+ threadId = decodeURIComponent(conversationActivityMatch[1]);
909
+ }
910
+ catch { /* invalid path */ }
911
+ const conversation = threadId ? await resolvedConversationCatalog.getConversation(threadId) : undefined;
912
+ if (!conversation) {
913
+ res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
914
+ res.end(JSON.stringify({ error: 'conversation_not_found' }));
915
+ return;
916
+ }
917
+ res.writeHead(200, {
918
+ 'Content-Type': 'application/json; charset=utf-8',
919
+ 'Cache-Control': 'no-store',
920
+ });
921
+ res.end(JSON.stringify(buildConversationDetailActivitySnapshot(conversation)));
922
+ return;
923
+ }
904
924
  if (path === '/conversations') {
905
925
  const html = renderConversationIndexPage(await resolvedConversationCatalog.listConversations(), lang);
906
926
  res.writeHead(200, {
@@ -1069,7 +1089,10 @@ export function createReportServer({ port, host: hostOption, reportsDir, analyse
1069
1089
  res.end(lang === 'en' ? 'conversation not found' : '对话不存在');
1070
1090
  return;
1071
1091
  }
1072
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
1092
+ res.writeHead(200, {
1093
+ 'Content-Type': 'text/html; charset=utf-8',
1094
+ 'Cache-Control': 'no-store',
1095
+ });
1073
1096
  res.end(renderConversationDetailPage(conversation, lang));
1074
1097
  return;
1075
1098
  }
@@ -371,6 +371,10 @@ export interface ExperienceTimelineEvent extends ExperienceEvidenceRef {
371
371
  toolStatus?: ToolCallStatus;
372
372
  isError?: boolean;
373
373
  fullText?: string;
374
+ attachments?: Array<{
375
+ attachmentKind: 'image' | 'file';
376
+ name: string;
377
+ }>;
374
378
  }
375
379
  export type DebugKnowledgeKind = 'project_instruction' | 'skill' | 'runtime_evidence';
376
380
  export type DebugKnowledgeAccessKind = 'injected' | 'read' | 'returned';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-knowledge",
3
- "version": "0.52.2",
3
+ "version": "0.52.3",
4
4
  "packageManager": "yarn@4.16.0",
5
5
  "description": "OMK — Observe. Measure. Know. Evidence-backed knowledge changes for AI applications.",
6
6
  "type": "module",
@@ -99,11 +99,11 @@
99
99
  "license": "MIT",
100
100
  "dependencies": {
101
101
  "@anthropic-ai/claude-agent-sdk": "^0.3.143",
102
- "@anthropic-ai/sdk": "^0.115.0",
102
+ "@anthropic-ai/sdk": "^0.117.1",
103
103
  "@inquirer/prompts": "^8.4.3",
104
104
  "@modelcontextprotocol/sdk": "^1.30.0",
105
105
  "@oclif/core": "^4",
106
- "@openai/codex-sdk": "0.146.0",
106
+ "@openai/codex-sdk": "0.147.0",
107
107
  "ajv": "^8.18.0",
108
108
  "chart.js": "^4.5.1",
109
109
  "es-module-lexer": "^2.0.0",
@@ -119,7 +119,7 @@
119
119
  "@types/node": "^25.5.0",
120
120
  "eslint": "^10.1.0",
121
121
  "husky": "^9.1.7",
122
- "lint-staged": "17.2.0",
122
+ "lint-staged": "17.3.0",
123
123
  "npm-run-all2": "^9.0.3",
124
124
  "typescript": "^6.0.2",
125
125
  "typescript-eslint": "^8.58.0",