@retaw106/mmc-pi 2.1.0 → 2.1.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/dist/index.js CHANGED
@@ -33757,6 +33757,8 @@ function applyPiHeuristicCleanup(sessionId, db, targets, piMessages, config, pre
33757
33757
  continue;
33758
33758
  const strippedSource = stripTagPrefix(stripped);
33759
33759
  if (strippedSource.trim().length === 0) {
33760
+ if (target.thinkingDropProtected)
33761
+ continue;
33760
33762
  const dropResult = target.drop?.() ?? "absent";
33761
33763
  const didReplace = dropResult === "absent" ? target.setContent(`[dropped §${tag.tagNumber}§]`) : false;
33762
33764
  if (dropResult === "removed" || dropResult === "absent") {
@@ -49874,7 +49876,7 @@ function buildStatusViewFor(check, options) {
49874
49876
  // package.json
49875
49877
  var package_default = {
49876
49878
  name: "@retaw106/mmc-pi",
49877
- version: "2.1.0",
49879
+ version: "2.1.1",
49878
49880
  type: "module",
49879
49881
  description: "Pi and OMP coding agent extension for Magic Context — cross-session memory and context management",
49880
49882
  main: "dist/index.js",
@@ -53153,7 +53155,7 @@ Returns the raw transcript as [N] U:/A: lines, capped at ~15K tokens; an oversiz
53153
53155
 
53154
53156
  Finer recovery:
53155
53157
  - verbose=true lists each message separately with its ordinal and a per-part preview (tool calls with output sizes) so you can pick one.
53156
- - message=N returns that one message in full — every text part and every tool call's complete input and output — from stored history. This is the way back to a tool output you released with mmc_reduce; if the message was deleted from history it says so.`;
53158
+ - message=N returns that one message in full — every text part and every tool call's complete input and output — from stored history. This is the way back to a tool output you released with mmc_reduce; if the message is no longer in stored history it says so.`;
53157
53159
  var CTX_EXPAND_TOKEN_BUDGET = 15000;
53158
53160
 
53159
53161
  // ../plugin/src/features/magic-context/tag-input.ts
@@ -53379,7 +53381,7 @@ function renderMessageByOrdinal(sessionId, ordinal) {
53379
53381
  });
53380
53382
  const msg = found[0];
53381
53383
  if (!msg) {
53382
- return `No message at ordinal ${ordinal} in this session's stored history — it was deleted ` + `(session prune/revert) or the ordinal is wrong, so it can't be recovered. ` + `Re-run the tool if you still need the data.`;
53384
+ return `No message at ordinal ${ordinal} is currently in this session's stored history — ` + `it may have been removed by a session prune/revert, or the ordinal may be wrong. ` + `Re-run the tool if you still need the data.`;
53383
53385
  }
53384
53386
  const rendered = msg.parts.map(renderPartFull).filter((l) => l !== null);
53385
53387
  const lines = [`[${msg.ordinal}] ${roleLabel(msg.role)} — full recovery:`, ""];
@@ -53452,7 +53454,10 @@ function renderItemByTag(db, sessionId, number, textIndexDomain = "part") {
53452
53454
  const part = message.parts[index];
53453
53455
  const matching = derived ? piText.filter((part) => isRecord7(part) && typeof part.text === "string" && createHash19("sha256").update(part.text).digest("hex") === derived[3]) : [];
53454
53456
  const selected = derived ? matching[Number(derived[4])] : textIndexDomain === "text" ? piText[index] : part;
53455
- return selected ? renderPartFull(selected) ?? "" : `Tag ${number}'s original text part is no longer in stored history.`;
53457
+ if (!selected)
53458
+ return `Tag ${number}'s original text part is no longer in stored history.`;
53459
+ const renderedPart = renderPartFull(selected);
53460
+ return renderedPart !== null ? renderedPart : `Tag ${number}'s stored text part has no renderable content (empty or structural-only); the requested content was not recovered.`;
53456
53461
  }
53457
53462
 
53458
53463
  // ../plugin/src/tools/unwrap-imitated-reduced-args.ts
@@ -53936,6 +53941,9 @@ function createCtxExpandTool(deps) {
53936
53941
  }
53937
53942
  return ok(lines.join(`
53938
53943
  `));
53944
+ } catch (error) {
53945
+ const message = error instanceof Error ? error.message : String(error);
53946
+ return err(`Error: mmc_expand failed to read this session's stored history: ${message}`);
53939
53947
  } finally {
53940
53948
  unregister();
53941
53949
  }
package/dist/web/main.js CHANGED
@@ -3149,7 +3149,9 @@ function listProjectSessions(store, projectPath) {
3149
3149
  sp.harness AS harness,
3150
3150
  MAX(sp.updated_at) AS last_activity,
3151
3151
  (SELECT COUNT(*) FROM compartments c
3152
- WHERE c.session_id = sp.session_id AND c.harness = sp.harness) AS compartment_count
3152
+ WHERE c.session_id = sp.session_id AND c.harness = sp.harness) AS compartment_count,
3153
+ (SELECT is_subagent FROM session_meta sm
3154
+ WHERE sm.session_id = sp.session_id LIMIT 1) AS is_subagent
3153
3155
  FROM session_projects sp
3154
3156
  WHERE sp.project_path = ?
3155
3157
  GROUP BY sp.session_id, sp.harness
@@ -3159,7 +3161,8 @@ function listProjectSessions(store, projectPath) {
3159
3161
  sessionId: r.session_id,
3160
3162
  harness: r.harness,
3161
3163
  lastActivityAt: typeof r.last_activity === "number" ? r.last_activity : null,
3162
- compartmentCount: typeof r.compartment_count === "number" ? r.compartment_count : 0
3164
+ compartmentCount: typeof r.compartment_count === "number" ? r.compartment_count : 0,
3165
+ processingEligibility: r.is_subagent === 1 ? "reduced-subagent" : r.is_subagent === 0 ? "primary" : "unknown"
3163
3166
  };
3164
3167
  });
3165
3168
  }
@@ -3371,6 +3374,7 @@ function seedContextDb(dbPath, options) {
3371
3374
  initializeDatabase(db);
3372
3375
  runMigrations(db);
3373
3376
  seedSessionProjects(db);
3377
+ seedPanelSessionFacts(db);
3374
3378
  seedCompartments(db);
3375
3379
  seedNotes(db);
3376
3380
  seedMemories(db);
@@ -3381,6 +3385,19 @@ function seedContextDb(dbPath, options) {
3381
3385
  db.close();
3382
3386
  }
3383
3387
  }
3388
+ function seedPanelSessionFacts(db) {
3389
+ const meta = db.prepare("INSERT INTO session_meta (session_id, harness, is_subagent) VALUES (?, ?, ?)");
3390
+ meta.run(FIXTURE_SEED.oc2Session, "opencode2", 0);
3391
+ meta.run(FIXTURE_SEED.treeChainRoot, "opencode2", 0);
3392
+ meta.run(FIXTURE_SEED.treeChainChild, "opencode2", 1);
3393
+ meta.run(FIXTURE_SEED.treeChainGrandchild, "opencode2", 1);
3394
+ meta.run(FIXTURE_SEED.piSessionId, "pi", 1);
3395
+ const tags = db.prepare("INSERT INTO tags (session_id, message_id, type, status, byte_size, tag_number, harness) VALUES (?, ?, 'message', 'active', 10, ?, ?)");
3396
+ tags.run(FIXTURE_SEED.oc2Session, "msg_fixture_u1:p0", 101, "opencode2");
3397
+ tags.run(FIXTURE_SEED.oc2Session, "msg_fixture_a4:p0", 102, "opencode2");
3398
+ tags.run(FIXTURE_SEED.piSessionId, "aaaaaaaa:p0", 201, "pi");
3399
+ db.prepare("INSERT INTO tags (session_id, message_id, type, status, byte_size, tag_number, harness, tool_owner_message_id) VALUES (?, ?, 'tool', 'active', 10, ?, ?, ?)").run(FIXTURE_SEED.piSessionId, "call-pi-1", 202, "pi", "bbbbbbbb");
3400
+ }
3384
3401
  function seedSessionProjects(db) {
3385
3402
  const insert = db.prepare("INSERT INTO session_projects (session_id, harness, project_path, updated_at) VALUES (?, ?, ?, ?)");
3386
3403
  insert.run(FIXTURE_SEED.oc2Session, "opencode2", FIXTURE_SEED.projectCore, T0 + 9000);
@@ -3523,7 +3540,8 @@ function seedOc2Store(dbPath, generation) {
3523
3540
  }
3524
3541
  var PI_BRANCH_FILES = [
3525
3542
  "2026-01-01T00-00-07-000Z_f1a0c0de-0000-4000-8000-000000000001.jsonl",
3526
- "2026-01-01T00-00-09-000Z_f1a0c0de-0000-4000-8000-000000000001.jsonl"
3543
+ "2026-01-01T00-00-09-000Z_f1a0c0de-0000-4000-8000-000000000001.jsonl",
3544
+ "2026-01-01T00-00-11-000Z_f1a0c0de-0000-4000-8000-000000000001.jsonl"
3527
3545
  ];
3528
3546
  function seedPiSessions(sessionsRoot) {
3529
3547
  const dir = join4(sessionsRoot, "--fixture-project--");
@@ -3576,6 +3594,21 @@ function seedPiSessions(sessionsRoot) {
3576
3594
  }),
3577
3595
  ""
3578
3596
  ].join(`
3597
+ `));
3598
+ writeFileSync(join4(dir, PI_BRANCH_FILES[2]), [
3599
+ header,
3600
+ entry("orphan-result", null, "toolResult", {
3601
+ toolCallId: "call-pi-1",
3602
+ content: [
3603
+ {
3604
+ type: "tool",
3605
+ callID: "call-pi-1",
3606
+ state: { output: "fixture: 无法可靠归属的结果" }
3607
+ }
3608
+ ]
3609
+ }),
3610
+ ""
3611
+ ].join(`
3579
3612
  `));
3580
3613
  writeFileSync(join4(dir, "2026-01-01T00-10-00-000Z_f1a0c0de-0000-4000-8000-000000000002.jsonl"), [
3581
3614
  headerFor(FIXTURE_SEED.piTreeSessionId, "2026-01-01T00:10:00.000Z"),
@@ -5209,6 +5242,9 @@ function listProjectSessionViews(sources, projectPath, options = {}) {
5209
5242
  title,
5210
5243
  parentId: meta?.parentId ?? null,
5211
5244
  forkSessionId: meta?.forkSessionId ?? null,
5245
+ registered: true,
5246
+ childIdentity: isPi || !meta ? "unknown" : meta.parentId ? "child" : "root",
5247
+ processingEligibility: row.processingEligibility,
5212
5248
  hostTimeCreated: meta?.timeCreated ?? null,
5213
5249
  hostTimeUpdated: meta?.timeUpdated ?? null,
5214
5250
  maintenance: isPi ? null : classifyMaintenance(title),
@@ -5433,6 +5469,7 @@ function oc2MessagePage(sources, sessionId, options) {
5433
5469
  }
5434
5470
  const last = kept.at(-1);
5435
5471
  const partsById = new Map(rawMessagePage(kept, 0).map((message) => [message.id, message.parts]));
5472
+ const tagNumbers = tagNumbersByMessageId(sources, sessionId, kept.map((row) => row.id), new Map(kept.map((row) => [row.id, extractToolCallIds(partsById.get(row.id))])));
5436
5473
  return {
5437
5474
  source: "oc2-host",
5438
5475
  rows: kept.map((row) => ({
@@ -5440,7 +5477,12 @@ function oc2MessagePage(sources, sessionId, options) {
5440
5477
  id: row.id,
5441
5478
  role: row.type,
5442
5479
  timeCreated: typeof row.time_created === "number" ? row.time_created : null,
5443
- parts: truncateParts(partsById.get(row.id), outputLimit)
5480
+ parts: truncateParts(partsById.get(row.id), outputLimit),
5481
+ coordinates: {
5482
+ hostSeq: row.seq,
5483
+ rawOrdinal: reader.messageOrdinalById(sessionId, row.id),
5484
+ tagNumbers: tagNumbers.get(row.id) ?? null
5485
+ }
5444
5486
  })),
5445
5487
  nextAfter: last ? last.seq : options.after ?? null,
5446
5488
  hasMore
@@ -5468,6 +5510,7 @@ function piMessagePage(sources, sessionId, options) {
5468
5510
  const end = options.end;
5469
5511
  const selected = projection.messages.filter((message) => message.ordinal > after && (end === undefined || message.ordinal <= end));
5470
5512
  const page = selected.slice(0, limit);
5513
+ const tagNumbers = tagNumbersByMessageId(sources, sessionId, page.map((message) => message.id), new Map(page.map((message) => [message.id, extractToolCallIds(message.parts)])));
5471
5514
  return {
5472
5515
  source: "pi-jsonl",
5473
5516
  branch: { fileName: file.fileName, directoryName: file.directoryName },
@@ -5476,12 +5519,96 @@ function piMessagePage(sources, sessionId, options) {
5476
5519
  id: message.id,
5477
5520
  role: message.role,
5478
5521
  timeCreated: null,
5479
- parts: truncateParts(message.parts, outputLimit)
5522
+ parts: truncateParts(message.parts, outputLimit),
5523
+ coordinates: {
5524
+ hostSeq: null,
5525
+ rawOrdinal: message.ordinal,
5526
+ tagNumbers: tagNumbers.get(message.id) ?? null
5527
+ }
5480
5528
  })),
5481
5529
  nextAfter: page.length > 0 ? page[page.length - 1].ordinal : after,
5482
5530
  hasMore: selected.length > limit
5483
5531
  };
5484
5532
  }
5533
+ function tagNumbersByMessageId(sources, sessionId, messageIds, toolCallIdsByMessageId = new Map) {
5534
+ const result = new Map(messageIds.map((id) => [id, []]));
5535
+ const uncertain = new Set;
5536
+ const opened = openContextFor(sources);
5537
+ if (!opened.store)
5538
+ return new Map(messageIds.map((id) => [id, null]));
5539
+ try {
5540
+ for (let offset = 0;offset < messageIds.length; offset += 100) {
5541
+ const batch = messageIds.slice(offset, offset + 100);
5542
+ if (batch.length === 0)
5543
+ continue;
5544
+ const predicates = batch.map(() => "(message_id = ? OR (message_id >= ? AND message_id < ?) OR tool_owner_message_id = ?)").join(" OR ");
5545
+ const parameters = batch.flatMap((id) => [id, `${id}:`, `${id};`, id]);
5546
+ const rows = opened.store.db.prepare(`SELECT tag_number, type, message_id, tool_owner_message_id
5547
+ FROM tags
5548
+ WHERE session_id = ? AND tag_number IS NOT NULL AND (${predicates})`).all(sessionId, ...parameters);
5549
+ for (const row of rows) {
5550
+ if (!Number.isSafeInteger(row.tag_number))
5551
+ continue;
5552
+ const owner = row.tool_owner_message_id;
5553
+ const exact = row.message_id;
5554
+ if (row.type === "tool") {
5555
+ if (owner && result.has(owner))
5556
+ result.get(owner)?.push(row.tag_number);
5557
+ continue;
5558
+ }
5559
+ const target = exact ? batch.find((id) => exact === id || exact >= `${id}:` && exact < `${id};`) : undefined;
5560
+ if (target !== undefined)
5561
+ result.get(target)?.push(row.tag_number);
5562
+ }
5563
+ }
5564
+ const toolCallIds = [
5565
+ ...new Set(messageIds.flatMap((id) => toolCallIdsByMessageId.get(id) ?? []))
5566
+ ];
5567
+ for (let offset = 0;offset < toolCallIds.length; offset += 100) {
5568
+ const batch = toolCallIds.slice(offset, offset + 100);
5569
+ const placeholders = batch.map(() => "?").join(", ");
5570
+ const rows = opened.store.db.prepare(`SELECT tag_number, message_id, tool_owner_message_id
5571
+ FROM tags
5572
+ WHERE session_id = ? AND type = 'tool' AND message_id IN (${placeholders})`).all(sessionId, ...batch);
5573
+ for (const row of rows) {
5574
+ if (!Number.isSafeInteger(row.tag_number))
5575
+ continue;
5576
+ for (const messageId of messageIds) {
5577
+ if (!(toolCallIdsByMessageId.get(messageId) ?? []).includes(row.message_id)) {
5578
+ continue;
5579
+ }
5580
+ if (row.tool_owner_message_id === messageId) {
5581
+ result.get(messageId)?.push(row.tag_number);
5582
+ } else {
5583
+ uncertain.add(messageId);
5584
+ }
5585
+ }
5586
+ }
5587
+ }
5588
+ for (const [id, numbers] of result) {
5589
+ result.set(id, uncertain.has(id) ? null : [...new Set(numbers ?? [])].sort((left, right) => left - right));
5590
+ }
5591
+ return result;
5592
+ } catch {
5593
+ return new Map(messageIds.map((id) => [id, null]));
5594
+ } finally {
5595
+ opened.store.db.close();
5596
+ }
5597
+ }
5598
+ function extractToolCallIds(parts) {
5599
+ if (!Array.isArray(parts))
5600
+ return [];
5601
+ return parts.flatMap((part) => {
5602
+ if (!part || typeof part !== "object" || Array.isArray(part))
5603
+ return [];
5604
+ const record = part;
5605
+ const type = typeof record.type === "string" ? record.type : "";
5606
+ if (type !== "tool" && type !== "toolCall" && type !== "tool_use" && type !== "tool_result")
5607
+ return [];
5608
+ const id = record.callID ?? record.toolCallId ?? record.id;
5609
+ return typeof id === "string" && id.length > 0 ? [id] : [];
5610
+ });
5611
+ }
5485
5612
  function isRawType(type) {
5486
5613
  return RAW_MESSAGE_TYPES.includes(type);
5487
5614
  }
@@ -5576,9 +5703,33 @@ function oc2AnchorsAndCounts(sources, sessionId, rows) {
5576
5703
  if (!opened.store)
5577
5704
  return { anchors, counts };
5578
5705
  try {
5579
- const messages = opened.store.metaDb.prepare("SELECT id, seq FROM session_message WHERE session_id = ?").all(sessionId);
5706
+ const boundaryIds = [
5707
+ ...new Set(rows.flatMap((row) => [row.start_message_id, row.end_message_id]).filter(Boolean))
5708
+ ];
5709
+ if (boundaryIds.length === 0)
5710
+ return { anchors, counts };
5711
+ const messages = [];
5712
+ for (let offset = 0;offset < boundaryIds.length; offset += 400) {
5713
+ const batch = boundaryIds.slice(offset, offset + 400);
5714
+ const placeholders = batch.map(() => "?").join(", ");
5715
+ messages.push(...opened.store.metaDb.prepare(`SELECT id, seq FROM session_message
5716
+ WHERE session_id = ? AND id IN (${placeholders})`).all(sessionId, ...batch));
5717
+ }
5718
+ const tagNumbers = tagNumbersByMessageId(sources, sessionId, boundaryIds);
5719
+ const rawOrdinals = new Map(messages.map((message) => [
5720
+ message.id,
5721
+ opened.store.reader.messageOrdinalById(sessionId, message.id)
5722
+ ]));
5580
5723
  for (const message of messages) {
5581
- anchors.set(message.id, { state: "resolved", anchor: message.seq });
5724
+ anchors.set(message.id, {
5725
+ state: "resolved",
5726
+ anchor: message.seq,
5727
+ coordinates: {
5728
+ hostSeq: message.seq,
5729
+ rawOrdinal: rawOrdinals.get(message.id) ?? null,
5730
+ tagNumbers: tagNumbers.get(message.id) ?? null
5731
+ }
5732
+ });
5582
5733
  }
5583
5734
  const queries = [];
5584
5735
  for (const row of rows) {
@@ -5613,6 +5764,7 @@ function piAnchorsAndCounts(sources, sessionId, rows, branch) {
5613
5764
  if (projection.status.kind !== "ok")
5614
5765
  return { anchors, counts };
5615
5766
  const aliases = sources.piAliasBuilder?.(projection.entries);
5767
+ const tagNumbers = tagNumbersByMessageId(sources, sessionId, projection.messages.map((message) => message.id));
5616
5768
  for (const row of rows) {
5617
5769
  for (const messageId of [row.start_message_id, row.end_message_id]) {
5618
5770
  if (messageId === "")
@@ -5620,7 +5772,7 @@ function piAnchorsAndCounts(sources, sessionId, rows, branch) {
5620
5772
  const resolved = resolvePiBoundary(messageId, projection.messages, aliases);
5621
5773
  if (resolved.state === "empty-id")
5622
5774
  continue;
5623
- anchors.set(messageId, boundaryFromResolution(resolved));
5775
+ anchors.set(messageId, boundaryFromResolution(resolved, tagNumbers.get(resolved.messageId) ?? null));
5624
5776
  }
5625
5777
  }
5626
5778
  const prefix = buildOrdinalPrefix(projection.messages);
@@ -5633,15 +5785,20 @@ function piAnchorsAndCounts(sources, sessionId, rows, branch) {
5633
5785
  }
5634
5786
  return { anchors, counts };
5635
5787
  }
5636
- function boundaryFromResolution(resolution) {
5788
+ function boundaryFromResolution(resolution, tagNumbers) {
5637
5789
  if (resolution.state === "resolved") {
5638
- return { state: "resolved", anchor: resolution.ordinal };
5790
+ return {
5791
+ state: "resolved",
5792
+ anchor: resolution.ordinal,
5793
+ coordinates: { hostSeq: null, rawOrdinal: resolution.ordinal, tagNumbers }
5794
+ };
5639
5795
  }
5640
5796
  if (resolution.state === "synth-resolved") {
5641
5797
  return {
5642
5798
  state: "synth-resolved",
5643
5799
  anchor: resolution.ordinal,
5644
- aliasedTo: resolution.aliasedTo
5800
+ aliasedTo: resolution.aliasedTo,
5801
+ coordinates: { hostSeq: null, rawOrdinal: resolution.ordinal, tagNumbers }
5645
5802
  };
5646
5803
  }
5647
5804
  return { state: "unresolved" };
@@ -13,3 +13,4 @@
13
13
  .comp-counts{margin-top:2px;color:#77837b;font:9px var(--mono);cursor:help}.expandable-text{white-space:pre-wrap;overflow-wrap:anywhere}.expandable-preview,.expandable-full{white-space:inherit}.expandable-preview[hidden],.expandable-full[hidden]{display:none}.preview-toggle{border:0;background:none;color:var(--accent);padding:4px 3px;min-height:32px;cursor:pointer;font:10px var(--mono);vertical-align:baseline}.preview-toggle:hover{text-decoration:underline}.preview-toggle:active{transform:scale(.96)}
14
14
  .session-counts,.memory-counts{padding:8px 12px;color:#aeb9b0;font:10px var(--mono);border-bottom:1px solid #27312d;background:#111614;white-space:nowrap}.session-tree-node{border-bottom:1px solid #222b27;padding:7px 8px 7px 10px;transition:background .16s,border-color .16s}.session-tree-node:hover{background:#1b231f}.session-tree-node.selected{background:#263321;border-left:3px solid var(--accent);padding-left:7px}.session-tree-node.context-ancestor{opacity:.68}.session-row{display:flex;align-items:flex-start;gap:6px;min-width:0}.session-toggle,.session-toggle-spacer{width:30px;height:30px;min-width:30px;display:grid;place-items:center;border:1px solid #344139;border-radius:7px;background:#171e1a;color:var(--accent);font:16px var(--mono);cursor:pointer}.session-toggle:hover{background:#263321;border-color:#70845b}.session-toggle-spacer{border-color:transparent;background:transparent}.session-open{display:grid;grid-template-columns:minmax(0,1fr);gap:3px;flex:1;min-width:0;padding:2px 3px;border:0;background:none;text-align:left;cursor:pointer}.session-open:hover .item-title{color:#d9ff9c}.session-open .item-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.session-open .item-meta{display:flex;justify-content:space-between;gap:8px}.session-time{color:#b7c5b7;white-space:nowrap}.session-time[title]{cursor:help}.session-children{margin:5px 0 0 19px;padding-left:8px;border-left:1px solid #3b493e}.session-children[hidden]{display:none}.session-tree-node[aria-level="3"]{padding-top:5px;padding-bottom:5px}.session-tree-node[aria-level="3"] .session-open .item-title{font-size:11px}.context-label,.relation-note{color:#aab89c;font:9px var(--mono)}.relation-note{color:var(--amber)}.maintenance-badge{color:#f0ce91;background:#3b2d1b;border-color:#74582f}.orphan-group{margin:10px;border:1px solid #5a4930;border-radius:9px;background:#1a1712;overflow:hidden}.orphan-heading{display:flex;align-items:center;gap:7px;padding:8px 9px;color:#efd69d}.orphan-heading strong{font-size:11px}.orphan-body{border-top:1px solid #4a3c29}.orphan-body[hidden]{display:none}.orphan-body .session-tree-node{border-bottom-color:#3d3427}.memory-category-options{display:flex;flex-wrap:wrap;gap:5px;padding:3px 0}.category-chip{display:inline-flex;align-items:center;gap:5px;padding:5px 7px;border:1px solid #334038;border-radius:999px;background:#171e1a;color:#aeb9b0;font-size:10px;cursor:pointer}.category-chip:has(input:checked){border-color:#849d62;background:#293522;color:#e2f7c5}.category-chip input{accent-color:var(--accent);margin:0}.memory-actions{align-items:center}.list-error{margin:10px;border:1px solid #704038;background:#2a1c1a;border-radius:9px;color:#f3c0b7}.list-error strong{color:#ffb8aa}.list-error .link-button{margin-top:7px}.item-list>.item-card{margin-left:10px;margin-right:10px}.branch-box{min-width:0}.session-open .item-sub{overflow-wrap:anywhere}.session-tree-node button:active,.orphan-heading button:active{transform:scale(.96)}
15
15
  @media(max-width:760px){.session-counts,.memory-counts{white-space:normal;line-height:1.6}.control-row{flex-wrap:wrap}.list-controls{gap:7px}.memory-category-options{padding:2px 0}.category-chip{min-height:34px}.session-children{margin-left:12px}.session-toggle,.session-toggle-spacer{width:34px;min-width:34px;height:34px}.session-open{padding-top:4px;padding-bottom:4px}}
16
+ .session-facts{color:#b9c9a9}.bound-coordinates{color:#9eaca2;font:9px var(--mono);overflow-wrap:anywhere}.message-index{overflow-wrap:anywhere}
@@ -88,6 +88,18 @@
88
88
  }
89
89
  const sessionTitle = (session) => session.title || (session.harness === "pi" ? "Pi 会话 · 无标题" : "OpenCode 2 会话 · 无标题");
90
90
  const number = (value) => value == null ? "—" : new Intl.NumberFormat("zh-CN").format(value);
91
+ function coordinateText(coordinates) {
92
+ const host = coordinates?.hostSeq == null ? "宿主序未知" : `宿主序 #${number(coordinates.hostSeq)}`;
93
+ const raw = coordinates?.rawOrdinal == null ? "原文序未知" : `原文序 #${number(coordinates.rawOrdinal)}`;
94
+ const tags = coordinates?.tagNumbers == null ? "标签未知" : coordinates.tagNumbers.length ? `标签 §${coordinates.tagNumbers.join("、")}§` : "无标签";
95
+ return `${host} · ${raw} · ${tags}`;
96
+ }
97
+ function sessionFacts(session) {
98
+ const identity = session?.childIdentity === "child" ? "子会话" : session?.childIdentity === "root" ? "主会话" : "身份未知";
99
+ const eligibility = session?.processingEligibility === "reduced-subagent" ? "子代理精简资格" : session?.processingEligibility === "primary" ? "主会话资格" : "资格未知";
100
+ return `已登记 · ${identity} · ${eligibility}`;
101
+ }
102
+ const SESSION_FACTS_TITLE = "登记资格,不保证当前启用或健康";
91
103
  const setReadAt = (data) => { if (data?.readAt) $("#read-at").textContent = `读取于 ${formatTime(data.readAt)}`; };
92
104
  async function api(url) {
93
105
  const response = await fetch(url, { method: "GET", cache: "no-store", headers: { Accept: "application/json" } });
@@ -301,7 +313,7 @@
301
313
  if (children.length) { const toggle = btn(expanded ? "−" : "+", "toggle-session", "session-toggle"); toggle.dataset.sessionKey = key; toggle.setAttribute("aria-expanded", String(expanded)); toggle.setAttribute("aria-controls", id); toggle.setAttribute("aria-label", `${expanded ? "收起" : "展开"}${sessionTitle(session)}的子会话`); row.append(toggle); } else row.append(el("span", "session-toggle-spacer"));
302
314
  const open = btn("", "session", "session-open"); open.dataset.sid = session.sessionId; open.dataset.harness = session.harness; open.setAttribute("aria-label", `打开会话:${sessionTitle(session)}`); open.append(el("span", "item-title", sessionTitle(session)));
303
315
  const meta = el("span", "item-meta"); const time = el("time", "session-time", sessionTimeText(session)); time.title = sessionTimeTitle(session); meta.append(el("span", "", session.harness === "pi" ? "Pi" : "OpenCode 2"), time); open.append(meta);
304
- const foot = el("span", "item-sub", `${session.sessionId} · ${number(session.compartmentCount)} 段摘要${children.length ? ` · ${number(children.length)} 个子会话` : ""}`); open.append(foot); if (item.context) open.append(el("span", "context-label", "用于定位")); if (session.maintenance) open.append(el("span", "badge maintenance-badge", `维护 · ${session.maintenance}`)); if (session.relation?.state === "orphan") open.append(el("span", "relation-note", RELATION_REASON[session.relation.reason] || "未归属子会话")); if (session.forkSessionId) { const source = el("span", "item-sub", `来源:${session.forkSessionId}`); source.title = "forkSessionId 仅表示来源,不是父子关系"; open.append(source); } row.append(open); node.append(row);
316
+ const foot = el("span", "item-sub", `${session.sessionId} · ${number(session.compartmentCount)} 段摘要${children.length ? ` · ${number(children.length)} 个子会话` : ""}`); open.append(foot); const facts = el("span", "item-sub session-facts", sessionFacts(session)); facts.title = SESSION_FACTS_TITLE; open.append(facts); if (item.context) open.append(el("span", "context-label", "用于定位")); if (session.maintenance) open.append(el("span", "badge maintenance-badge", `维护 · ${session.maintenance}`)); if (session.relation?.state === "orphan") open.append(el("span", "relation-note", RELATION_REASON[session.relation.reason] || "未归属子会话")); if (session.forkSessionId) { const source = el("span", "item-sub", `来源:${session.forkSessionId}`); source.title = "forkSessionId 仅表示来源,不是父子关系"; open.append(source); } row.append(open); node.append(row);
305
317
  if (session.harness === "pi") { const branches = session.piBranches || []; const box = el("div", "branch-box"); box.append(el("span", "item-sub", branches.length ? `Pi 分支文件 · ${branches.length}` : "Pi 原文文件未找到")); for (const branch of branches) { const b = btn(`${branch.fileName} · ${branch.directoryName}`, "branch", "link-button branch-button"); b.dataset.sid = session.sessionId; b.dataset.harness = session.harness; b.dataset.branch = branch.fileName; b.title = branch.headerTimestamp || "时间未知"; box.append(b); } node.append(box); }
306
318
  if (children.length) { const group = el("div", "session-children"); group.id = id; group.setAttribute("role", "group"); group.hidden = !expanded; if (expanded) for (const child of children) renderSessionNode(group, child, level + 1); node.append(group); } parent.append(node);
307
319
  }
@@ -364,7 +376,7 @@
364
376
  const root = $("#detail"); root.classList.remove("detail-empty"); root.replaceChildren(); if (!state.session) return;
365
377
  const wrap = el("div", "detail-content"); addMobileBack(wrap, "list", "返回列表");
366
378
  const s = state.session; wrap.append(el("div", "detail-kicker", `${s.harness === "pi" ? "PI SESSION" : "OPENCODE 2 SESSION"}${state.branch ? " · BRANCH SELECTED" : ""}`));
367
- wrap.append(el("h2", "detail-title", sessionTitle(s))); const meta = el("div", "detail-meta"); meta.append(el("span", "badge", s.sessionId), el("span", "badge", `${number(s.compartmentCount)} 个摘要段`), el("span", "", sessionTimeText(s))); if (state.branch) meta.append(el("span", "badge", state.branch)); wrap.append(meta);
379
+ wrap.append(el("h2", "detail-title", sessionTitle(s))); const meta = el("div", "detail-meta"); const eligibility = el("span", "badge", sessionFacts(s).replace("已登记 · ", "")); eligibility.title = SESSION_FACTS_TITLE; meta.append(el("span", "badge", s.sessionId), el("span", "badge", "已登记"), eligibility, el("span", "badge", `${number(s.compartmentCount)} 个摘要段`), el("span", "", sessionTimeText(s))); if (state.branch) meta.append(el("span", "badge", state.branch)); wrap.append(meta);
368
380
  if (s.harness === "pi" && !state.branch) { const prompt = section(wrap, "选择 Pi 分支", "Pi 原文与摘要按分支独立读取"); prompt.append(el("p", "boundary-warning", "请回到会话卡,选择具体分支文件后再查看摘要和原文。面板不会猜测默认分支。")); root.append(wrap); return; }
369
381
  const comps = section(wrap, "摘要分段", "层级为审阅展示,不是存储字段");
370
382
  if (state.detailErrors?.compartments) errorBox(comps, state.detailErrors.compartments);
@@ -387,14 +399,15 @@
387
399
  const bounds = c.bounds || {}; const boundRow = el("div", "bound-row"); boundRow.append(el("span", "", "边界")); for (const [side, label] of [["start", "起点"], ["end", "终点"]]) {
388
400
  const bound = bounds[side] || { state: "unresolved" }; const stateName = bound.state === "synth-resolved" ? "经别名解析" : bound.state === "resolved" ? "已关联" : bound.state === "unlinked" ? "无边界 id" : "边界无法解析";
389
401
  boundRow.append(el("span", "badge", `${label} · ${stateName}`));
390
- if (bound.state === "resolved" || bound.state === "synth-resolved") { const jump = btn(`跳到${label} #${bound.anchor}`, "jump", "link-button"); jump.dataset.anchor = bound.anchor; boundRow.append(jump); if (bound.aliasedTo) boundRow.append(el("span", "section-note", `→ ${bound.aliasedTo}`)); }
402
+ if (bound.state === "resolved" || bound.state === "synth-resolved") { boundRow.append(el("span", "bound-coordinates", coordinateText(bound.coordinates))); const jump = btn(`跳到${label}`, "jump", "link-button"); jump.dataset.anchor = bound.anchor; boundRow.append(jump); if (bound.aliasedTo) boundRow.append(el("span", "section-note", `→ ${bound.aliasedTo}`)); } else boundRow.append(el("span", "bound-coordinates", coordinateText(null)));
391
403
  }
392
404
  card.append(boundRow);
393
- const range = compartmentRange(c); if (range) { const view = getSegmentView(c); const raw = el("details", "comp-raw"); raw.dataset.compartmentKey = segmentKey(c); raw.open = view.open; raw.append(el("summary", "link-summary", `展开该段原文 · ${range.start}–${range.end}`)); const body = el("div", "comp-message-panel"); raw.append(body); raw.addEventListener("toggle", () => { view.open = raw.open; if (raw.open) { if (view.skipToggleRequest) view.skipToggleRequest = false; else loadSegmentMessages(c, segmentKey(c), body); } }); card.append(raw); } else card.append(el("p", "boundary-warning", "原文归属:未分组。请在下方“边界未确认的原文”中按需读取。")); parent.append(card);
405
+ const range = compartmentRange(c); if (range) { const view = getSegmentView(c); const raw = el("details", "comp-raw"); raw.dataset.compartmentKey = segmentKey(c); raw.open = view.open; raw.append(el("summary", "link-summary", `展开该段原文 · ${compartmentRangeLabel(c)}`)); const body = el("div", "comp-message-panel"); raw.append(body); raw.addEventListener("toggle", () => { view.open = raw.open; if (raw.open) { if (view.skipToggleRequest) view.skipToggleRequest = false; else loadSegmentMessages(c, segmentKey(c), body); } }); card.append(raw); } else card.append(el("p", "boundary-warning", "原文归属:未分组。请在下方“边界未确认的原文”中按需读取。")); parent.append(card);
394
406
  }
395
407
  function segmentKey(c) { return String(c.id ?? `sequence-${c.sequence}`); }
396
408
  function numericBoundary(bound) { return (bound?.state === "resolved" || bound?.state === "synth-resolved") && Number.isSafeInteger(bound.anchor) ? bound.anchor : null; }
397
409
  function compartmentRange(c) { const start = numericBoundary(c.bounds?.start); const end = numericBoundary(c.bounds?.end); return start !== null && end !== null && start <= end ? { start, end } : null; }
410
+ function compartmentRangeLabel(c) { const start = c.bounds?.start?.coordinates; const end = c.bounds?.end?.coordinates; if (start?.hostSeq != null && end?.hostSeq != null) return `宿主序 ${start.hostSeq}–${end.hostSeq}`; if (start?.rawOrdinal != null && end?.rawOrdinal != null) return `原文序 ${start.rawOrdinal}–${end.rawOrdinal}`; return "坐标未知"; }
398
411
  function boundaryReason(c) { const bounds = c.bounds || {}; return ["start", "end"].map((side) => { const value = bounds[side]?.state; return `${side === "start" ? "起点" : "终点"}:${value === "unlinked" ? "无边界 id" : value === "synth-resolved" || value === "resolved" ? "数字边界无效" : "边界无法解析"}`; }).join(";"); }
399
412
  function getSegmentView(c) { const key = segmentKey(c); let view = state.segmentViews.get(key); if (!view) { const range = compartmentRange(c); view = { open: false, after: range ? range.start - 1 : null, cursors: [], page: null, loading: false, error: null, fullOutputs: false, openToolOutputs: new Set(), request: 0 }; state.segmentViews.set(key, view); } return view; }
400
413
  function renderUngroupedSection(parent, title, note, reason, initialAfter = null) {
@@ -408,7 +421,7 @@
408
421
  const role = row.role === "user" ? "用户" : row.role === "assistant" ? "助手" : row.role || "未知角色";
409
422
  head.append(el("strong", "message-role", role));
410
423
  const identity = el("span", "message-aux");
411
- identity.append(el("span", "message-index", `消息 #${row.key}`));
424
+ identity.append(el("span", "message-index", coordinateText(row.coordinates)));
412
425
  const messageId = String(row.id || "无 ID"); const idLabel = messageId.length > 32 ? `${messageId.slice(0, 15)}…${messageId.slice(-10)}` : messageId;
413
426
  const id = el("span", "message-id", idLabel); id.title = `消息 ID:${messageId}`; identity.append(id); head.append(identity, el("time", "message-time", formatTime(row.timeCreated))); card.append(head);
414
427
  for (const [index, part] of (row.parts || []).entries()) renderPart(card, part, row, index, scope);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retaw106/mmc-pi",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "type": "module",
5
5
  "description": "Pi and OMP coding agent extension for Magic Context — cross-session memory and context management",
6
6
  "main": "dist/index.js",