@imenam/mcp-documentor 1.0.37 → 1.0.40

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.
@@ -207,16 +207,21 @@ export class StorageEngine {
207
207
  return true;
208
208
  }
209
209
  async rewriteLinks(oldId, newId, project) {
210
- const docs = await this.listDocs(project);
210
+ const config = this.getConfig();
211
+ const proj = project ?? config.currentProject;
211
212
  const updated = [];
212
- for (const summary of docs) {
213
+ // Within the same project: rewrite same-project refs and explicit self-cross-project refs
214
+ const sameProjDocs = await this.listDocs(proj);
215
+ for (const summary of sameProjDocs) {
213
216
  if (summary.id === newId)
214
217
  continue;
215
- const doc = await this.getDoc(summary.id, project);
218
+ const doc = await this.getDoc(summary.id, proj);
216
219
  if (!doc)
217
220
  continue;
218
221
  let changed = false;
219
- const newContent = doc.content.replace(new RegExp(`doc://${escapeRegex(oldId)}`, "g"), `doc://${newId}`);
222
+ let newContent = doc.content.replace(new RegExp(`doc://${escapeRegex(oldId)}`, "g"), `doc://${newId}`);
223
+ // Also rewrite explicit self cross-project refs (doc://proj:oldId → doc://proj:newId)
224
+ newContent = newContent.replace(new RegExp(`doc://${escapeRegex(proj)}:${escapeRegex(oldId)}`, "g"), `doc://${proj}:${newId}`);
220
225
  if (newContent !== doc.content) {
221
226
  doc.content = newContent;
222
227
  changed = true;
@@ -226,15 +231,49 @@ export class StorageEngine {
226
231
  changed = true;
227
232
  return newId;
228
233
  }
234
+ if (ref === `${proj}:${oldId}`) {
235
+ changed = true;
236
+ return `${proj}:${newId}`;
237
+ }
229
238
  return ref;
230
239
  });
231
240
  doc.related_docs = newRelated;
232
241
  if (changed) {
233
242
  doc.updatedAt = new Date().toISOString();
234
- await this.saveDoc(doc, project);
243
+ await this.saveDoc(doc, proj);
235
244
  updated.push(summary.id);
236
245
  }
237
246
  }
247
+ // In other projects: rewrite cross-project refs pointing to this project
248
+ for (const otherProj of config.projects) {
249
+ if (otherProj === proj)
250
+ continue;
251
+ const otherDocs = await this.listDocs(otherProj);
252
+ for (const summary of otherDocs) {
253
+ const doc = await this.getDoc(summary.id, otherProj);
254
+ if (!doc)
255
+ continue;
256
+ let changed = false;
257
+ const newContent = doc.content.replace(new RegExp(`doc://${escapeRegex(proj)}:${escapeRegex(oldId)}`, "g"), `doc://${proj}:${newId}`);
258
+ if (newContent !== doc.content) {
259
+ doc.content = newContent;
260
+ changed = true;
261
+ }
262
+ const newRelated = doc.related_docs.map((ref) => {
263
+ if (ref === `${proj}:${oldId}`) {
264
+ changed = true;
265
+ return `${proj}:${newId}`;
266
+ }
267
+ return ref;
268
+ });
269
+ doc.related_docs = newRelated;
270
+ if (changed) {
271
+ doc.updatedAt = new Date().toISOString();
272
+ await this.saveDoc(doc, otherProj);
273
+ updated.push(`${otherProj}:${summary.id}`);
274
+ }
275
+ }
276
+ }
238
277
  return updated;
239
278
  }
240
279
  }
@@ -246,6 +246,13 @@ app.get("/api/projects", async (c) => {
246
246
  return c.json({ error: result.error }, 500);
247
247
  return c.json(result.data);
248
248
  });
249
+ app.get("/api/folders", async (c) => {
250
+ const project = c.req.query("project");
251
+ const result = await ipcRequest("LIST_FOLDERS", project ? { project } : {});
252
+ if (result.error)
253
+ return c.json({ error: result.error }, 500);
254
+ return c.json(result.data);
255
+ });
249
256
  app.put("/api/project", async (c) => {
250
257
  const { project } = await c.req.json();
251
258
  if (!project)
@@ -261,6 +268,27 @@ app.get("/api/docs", async (c) => {
261
268
  return c.json({ error: result.error }, 500);
262
269
  return c.json(result.data);
263
270
  });
271
+ app.post("/api/doc", async (c) => {
272
+ try {
273
+ const body = await c.req.json();
274
+ if (!body.title || !body.content) {
275
+ return c.json({ error: "title and content are required" }, 400);
276
+ }
277
+ const result = await ipcRequest("CREATE_DOC", {
278
+ title: body.title,
279
+ content: body.content,
280
+ folder: body.folder,
281
+ tags: body.tags,
282
+ project: body.project,
283
+ }, 15000);
284
+ if (result.error)
285
+ return c.json({ error: result.error }, 500);
286
+ return c.json(result.data ?? { success: true });
287
+ }
288
+ catch (err) {
289
+ return c.json({ error: err.message }, 500);
290
+ }
291
+ });
264
292
  app.get("/api/doc/*", async (c) => {
265
293
  const id = decodeURIComponent(c.req.path.slice("/api/doc/".length));
266
294
  const result = await ipcRequest("GET_DOC", { id });
package/dist/src/index.js CHANGED
@@ -11,7 +11,7 @@ import { GuiLauncher, createLogger, setupLogging } from "@imenam/mcp-gui-interfa
11
11
  import { StorageEngine } from "./core/storage-engine.js";
12
12
  import { EmbeddingStore } from "./core/embedding-store.js";
13
13
  import { EmbeddingEngine } from "./core/embedding-engine.js";
14
- import { createDoc } from "./tools/create-doc.js";
14
+ import { createDoc, slugify } from "./tools/create-doc.js";
15
15
  import { getDoc } from "./tools/get-doc.js";
16
16
  import { listDocs } from "./tools/list-docs.js";
17
17
  import { updateDoc } from "./tools/update-doc.js";
@@ -126,6 +126,12 @@ function handleIPCMessage(msg) {
126
126
  else if (msg.type === "GET_DOC_VERSION") {
127
127
  handleGetDocVersionIPC(msg);
128
128
  }
129
+ else if (msg.type === "CREATE_DOC") {
130
+ handleCreateDocIPC(msg);
131
+ }
132
+ else if (msg.type === "LIST_FOLDERS") {
133
+ handleListFoldersIPC(msg);
134
+ }
129
135
  else if (msg.type === "UPDATE_DOC") {
130
136
  handleUpdateDocIPC(msg);
131
137
  }
@@ -257,6 +263,89 @@ async function handleGetDoc(msg) {
257
263
  });
258
264
  }
259
265
  }
266
+ async function handleListFoldersIPC(msg) {
267
+ try {
268
+ const project = msg.data?.project;
269
+ const docs = await storageEngine.listDocs(project);
270
+ const folders = new Set();
271
+ for (const doc of docs) {
272
+ if (doc.folder) {
273
+ const parts = doc.folder.split("/").filter(Boolean);
274
+ for (let i = 1; i <= parts.length; i++) {
275
+ folders.add(parts.slice(0, i).join("/"));
276
+ }
277
+ }
278
+ }
279
+ broadcastToGUI({
280
+ type: "LIST_FOLDERS_RESPONSE",
281
+ correlationId: msg.correlationId,
282
+ data: { folders: Array.from(folders).sort() },
283
+ timestamp: new Date().toISOString(),
284
+ });
285
+ }
286
+ catch (err) {
287
+ broadcastToGUI({
288
+ type: "LIST_FOLDERS_RESPONSE",
289
+ correlationId: msg.correlationId,
290
+ error: err.message,
291
+ timestamp: new Date().toISOString(),
292
+ });
293
+ }
294
+ }
295
+ async function handleCreateDocIPC(msg) {
296
+ try {
297
+ const { title, content, folder, tags, project } = msg.data ?? {};
298
+ if (!title || !content)
299
+ throw new Error("title and content are required");
300
+ const result = await createDoc(storageEngine, embeddingEngine, {
301
+ title,
302
+ content,
303
+ ...(folder && { folder }),
304
+ ...(tags && { tags }),
305
+ ...(project && { project }),
306
+ });
307
+ if (result.isError) {
308
+ const text = result.content[0]?.type === "text" ? result.content[0].text : "Create failed";
309
+ broadcastToGUI({
310
+ type: "CREATE_DOC_RESPONSE",
311
+ correlationId: msg.correlationId,
312
+ error: text,
313
+ timestamp: new Date().toISOString(),
314
+ });
315
+ return;
316
+ }
317
+ const slug = slugify(title);
318
+ const id = folder ? `${folder}/${slug}` : slug;
319
+ broadcastToGUI({
320
+ type: "CREATE_DOC_RESPONSE",
321
+ correlationId: msg.correlationId,
322
+ data: { success: true, id },
323
+ timestamp: new Date().toISOString(),
324
+ });
325
+ const docs = await storageEngine.listDocs();
326
+ for (const doc of docs) {
327
+ const full = await storageEngine.getDoc(doc.id);
328
+ if (full && full.tracked_files.length > 0) {
329
+ const report = computeFreshness(full.tracked_files);
330
+ doc.isStale = report.status === "stale";
331
+ }
332
+ }
333
+ const config = storageEngine.getConfig();
334
+ broadcastToGUI({
335
+ type: "INITIAL_STATE",
336
+ data: { config, docs, projects: config.projects },
337
+ timestamp: new Date().toISOString(),
338
+ });
339
+ }
340
+ catch (err) {
341
+ broadcastToGUI({
342
+ type: "CREATE_DOC_RESPONSE",
343
+ correlationId: msg.correlationId,
344
+ error: err.message,
345
+ timestamp: new Date().toISOString(),
346
+ });
347
+ }
348
+ }
260
349
  async function handleUpdateDocIPC(msg) {
261
350
  try {
262
351
  const result = await updateDoc(storageEngine, embeddingEngine, {
@@ -28,13 +28,31 @@ function extractDocLinks(content) {
28
28
  }
29
29
  return ids;
30
30
  }
31
+ function parseCrossProjectRef(ref) {
32
+ const colonIdx = ref.indexOf(":");
33
+ if (colonIdx === -1)
34
+ return null;
35
+ return { project: ref.slice(0, colonIdx), docId: ref.slice(colonIdx + 1) };
36
+ }
31
37
  export async function checkLinks(storageEngine, args) {
32
38
  try {
33
39
  const validated = CheckLinksArgsSchema.parse(args);
34
40
  const config = storageEngine.getConfig();
35
41
  const project = validated.project ?? config.currentProject;
42
+ const knownProjects = new Set(config.projects);
36
43
  const docs = await storageEngine.listDocs(project);
37
44
  const validIds = new Set(docs.map((d) => d.id));
45
+ // Cache of validIds per project to avoid re-fetching
46
+ const projectValidIds = new Map();
47
+ projectValidIds.set(project, validIds);
48
+ async function getValidIdsForProject(proj) {
49
+ if (projectValidIds.has(proj))
50
+ return projectValidIds.get(proj);
51
+ const projDocs = await storageEngine.listDocs(proj);
52
+ const ids = new Set(projDocs.map((d) => d.id));
53
+ projectValidIds.set(proj, ids);
54
+ return ids;
55
+ }
38
56
  const brokenRefs = [];
39
57
  const brokenDocLinks = [];
40
58
  const brokenAnchors = [];
@@ -43,13 +61,37 @@ export async function checkLinks(storageEngine, args) {
43
61
  if (!doc)
44
62
  continue;
45
63
  for (const ref of doc.related_docs ?? []) {
46
- if (!validIds.has(ref)) {
47
- brokenRefs.push({ sourceId: summary.id, brokenId: ref });
64
+ const cross = parseCrossProjectRef(ref);
65
+ if (cross) {
66
+ if (!knownProjects.has(cross.project)) {
67
+ brokenRefs.push({ sourceId: summary.id, brokenId: ref, reason: `project "${cross.project}" not found` });
68
+ }
69
+ else {
70
+ const targetIds = await getValidIdsForProject(cross.project);
71
+ if (!targetIds.has(cross.docId)) {
72
+ brokenRefs.push({ sourceId: summary.id, brokenId: ref, reason: `doc "${cross.docId}" not found in project "${cross.project}"` });
73
+ }
74
+ }
75
+ }
76
+ else if (!validIds.has(ref)) {
77
+ brokenRefs.push({ sourceId: summary.id, brokenId: ref, reason: "doc not found" });
48
78
  }
49
79
  }
50
80
  for (const targetId of extractDocLinks(doc.content)) {
51
- if (!validIds.has(targetId)) {
52
- brokenDocLinks.push({ sourceId: summary.id, targetId });
81
+ const cross = parseCrossProjectRef(targetId);
82
+ if (cross) {
83
+ if (!knownProjects.has(cross.project)) {
84
+ brokenDocLinks.push({ sourceId: summary.id, targetId, reason: `project "${cross.project}" not found` });
85
+ }
86
+ else {
87
+ const targetIds = await getValidIdsForProject(cross.project);
88
+ if (!targetIds.has(cross.docId)) {
89
+ brokenDocLinks.push({ sourceId: summary.id, targetId, reason: `doc "${cross.docId}" not found in project "${cross.project}"` });
90
+ }
91
+ }
92
+ }
93
+ else if (!validIds.has(targetId)) {
94
+ brokenDocLinks.push({ sourceId: summary.id, targetId, reason: "doc not found" });
53
95
  }
54
96
  }
55
97
  const anchorLinks = extractAnchorLinks(doc.content);
@@ -75,10 +117,10 @@ export async function checkLinks(storageEngine, args) {
75
117
  }
76
118
  const lines = [];
77
119
  for (const b of brokenRefs) {
78
- lines.push(` [related_doc] ${b.sourceId} → ${b.brokenId} (doc not found)`);
120
+ lines.push(` [related_doc] ${b.sourceId} → ${b.brokenId} (${b.reason})`);
79
121
  }
80
122
  for (const b of brokenDocLinks) {
81
- lines.push(` [doc://] ${b.sourceId} → doc://${b.targetId} (doc not found)`);
123
+ lines.push(` [doc://] ${b.sourceId} → doc://${b.targetId} (${b.reason})`);
82
124
  }
83
125
  for (const b of brokenAnchors) {
84
126
  lines.push(` [anchor] ${b.sourceId} → #${b.anchor} (heading not found)`);
@@ -1,5 +1,6 @@
1
1
  import type { StorageEngine } from "../core/storage-engine.js";
2
2
  import type { EmbeddingEngine } from "../core/embedding-engine.js";
3
+ export declare function slugify(title: string): string;
3
4
  export declare function createDoc(storageEngine: StorageEngine, embeddingEngine: EmbeddingEngine, args: unknown): Promise<{
4
5
  isError: boolean;
5
6
  content: {
@@ -3,7 +3,7 @@ import path from "path";
3
3
  import { CreateDocArgsSchema } from "../../shared/schemas/doc.schema.js";
4
4
  import { computeFileHash } from "./check-freshness.js";
5
5
  import { snapshotSection } from "../core/section-tracker.js";
6
- function slugify(title) {
6
+ export function slugify(title) {
7
7
  return title
8
8
  .normalize("NFD")
9
9
  .replace(/[\u0300-\u036f]/g, "")
@@ -1 +1 @@
1
- export declare const APP_VERSION = "1.0.37";
1
+ export declare const APP_VERSION = "1.0.40";
@@ -1,2 +1,2 @@
1
1
  // Auto-generated version file - do not edit manually
2
- export const APP_VERSION = '1.0.37';
2
+ export const APP_VERSION = '1.0.40';
@@ -0,0 +1 @@
1
+ *{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;background:#0f1117;color:#e1e4e8;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}#root{width:100%;height:100vh}.dashboard{display:flex;flex-direction:column;height:100vh;background:#0f1117;color:#e1e4e8;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.dashboard-header{display:flex;align-items:center;justify-content:space-between;padding:10px 20px;background:#161b22;border-bottom:1px solid #30363d;gap:16px}.header-left{display:flex;align-items:center;gap:12px;flex-shrink:0}.header-title{font-size:18px;font-weight:600;margin:0;color:#f0f6fc}.connection-badge{font-size:11px;padding:2px 8px;border-radius:10px;font-weight:500}.connection-badge.connected{background:#1a3a2a;color:#3fb950}.connection-badge.disconnected{background:#3a1a1a;color:#f85149}.header-center{flex:1;max-width:500px}.header-right{display:flex;align-items:center;gap:8px;flex-shrink:0}.settings-btn{background:none;border:1px solid #30363d;color:#8b949e;font-size:18px;cursor:pointer;padding:4px 8px;border-radius:6px;transition:all .15s}.settings-btn:hover{background:#21262d;color:#f0f6fc}.stale-filter-btn{background:none;border:1px solid #30363d;color:#8b949e;font-size:12px;cursor:pointer;padding:4px 10px;border-radius:6px;transition:all .15s;white-space:nowrap}.stale-filter-btn:hover{background:#3d2a0066;border-color:#d29922;color:#d29922}.stale-filter-btn.active{background:#3d2a00;border-color:#d29922;color:#d29922;font-weight:600}.sidebar-toggle-btn{background:none;border:1px solid #30363d;color:#8b949e;font-size:12px;cursor:pointer;padding:4px 8px;border-radius:6px;transition:all .15s;line-height:1}.sidebar-toggle-btn:hover{background:#21262d;color:#f0f6fc}.dashboard-body{display:flex;flex:1;overflow:hidden;max-width:1700px;margin:0 auto}.sidebar-wrapper{width:450px;flex-shrink:0;overflow:hidden;transition:width .25s ease}.sidebar-wrapper.collapsed{width:0}.sidebar{width:450px;height:100%;border-right:1px solid #30363d;background:#161b22;overflow-y:auto;flex-shrink:0}.sidebar-item{padding:12px 16px;cursor:pointer;border-bottom:1px solid #21262d;transition:background .15s}.sidebar-item:hover{background:#1c2129}.sidebar-item.active{background:#1f6feb22;border-left:3px solid #1f6feb}.sidebar-item-title{font-size:14px;font-weight:500;color:#f0f6fc;margin-bottom:4px}.sidebar-item-meta{font-size:11px;color:#8b949e}.sidebar-tags{display:flex;gap:4px;flex-wrap:wrap;margin-top:4px}.tag-badge{font-size:10px;padding:1px 6px;border-radius:8px;background:#21262d;color:#8b949e}.stale-badge{font-size:10px;padding:1px 6px;border-radius:8px;font-weight:600}.stale-badge.stale{background:#3d2a00;color:#d29922}.stale-badge.fresh{background:#1a3a2a;color:#3fb950}.sidebar-empty{padding:24px 16px;text-align:center;color:#8b949e;font-size:13px}.sidebar-filter-header{display:flex;align-items:center;gap:8px;padding:10px 16px;border-bottom:1px solid #3d2a00;background:#1f1600;font-size:12px;font-weight:600;color:#d29922;text-transform:uppercase;letter-spacing:.05em;-webkit-user-select:none;user-select:none}.sidebar-filter-icon{font-size:13px}.sidebar-filter-count{margin-left:auto;background:#3d2a00;color:#d29922;font-size:11px;padding:1px 7px;border-radius:10px;font-weight:700}.sidebar-folder-header{padding:8px 16px;font-size:11px;font-weight:600;color:#8b949e;background:#0f1117;border-bottom:1px solid #21262d;cursor:pointer;display:flex;align-items:center;gap:6px;text-transform:uppercase;letter-spacing:.05em;-webkit-user-select:none;user-select:none}.sidebar-folder-header:hover{color:#c9d1d9}.folder-icon{font-size:9px;transition:transform .15s;display:inline-block}.folder-icon.open{transform:rotate(90deg)}.doc-viewer{flex:1;min-height:0;display:flex;flex-direction:column;overflow-y:auto;padding:24px 32px}.doc-viewer-empty{display:flex;align-items:center;justify-content:center;height:100%;color:#484f58;font-size:16px}.doc-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;padding-bottom:12px;border-bottom:1px solid #21262d}.doc-header-left{display:flex;align-items:center;gap:12px;min-width:0}.nav-buttons{display:flex;gap:4px;flex-shrink:0}.nav-btn{background:none;border:1px solid #30363d;color:#8b949e;font-size:16px;cursor:pointer;padding:3px 9px;border-radius:6px;transition:all .15s;line-height:1.4}.nav-btn:hover:not(:disabled){background:#21262d;color:#f0f6fc;border-color:#8b949e}.nav-btn:disabled{opacity:.3;cursor:default}.doc-title{font-size:24px;font-weight:600;color:#f0f6fc;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.doc-header-right{display:flex;align-items:center;gap:8px;flex-shrink:0}.freshness-btn{padding:6px 12px;border:1px solid #30363d;background:#21262d;color:#c9d1d9;border-radius:6px;cursor:pointer;font-size:12px;transition:all .15s}.freshness-btn:hover{background:#30363d;color:#f0f6fc}.download-btn{padding:6px 12px;border:1px solid #30363d;background:#21262d;color:#8b949e;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600;transition:all .15s;white-space:nowrap}.download-btn:hover{background:#1f6feb22;border-color:#1f6feb;color:#58a6ff}.copy-btn{padding:6px 12px;border:1px solid #30363d;background:#21262d;color:#8b949e;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600;transition:all .15s;white-space:nowrap}.copy-btn:hover{background:#1f6feb22;border-color:#1f6feb;color:#58a6ff}.copy-btn--copied,.copy-btn--copied:hover{background:#1a3a1f;border-color:#3fb950;color:#3fb950}.doc-related{display:flex;align-items:center;flex-wrap:wrap;gap:8px;padding:10px 14px;margin-bottom:20px;background:#161b22;border:1px solid #30363d;border-radius:6px;font-size:12px}.doc-related-label{color:#8b949e;font-weight:500;flex-shrink:0}.doc-related-link{padding:2px 10px;border:1px solid #1f6feb55;background:#1f6feb18;color:#58a6ff;border-radius:12px;cursor:pointer;font-size:12px;transition:all .15s}.doc-related-link:hover{background:#1f6feb33;border-color:#1f6feb}.doc-related-broken{padding:2px 10px;border:1px solid #f8514933;background:#f8514918;color:#f85149;border-radius:12px;font-size:12px;font-style:italic}.doc-related-link--cross-project{border-color:#6e40c955;background:#6e40c918;color:#d2a8ff}.doc-related-link--cross-project:hover{background:#6e40c933;border-color:#6e40c9}.doc-inline-link{display:inline;padding:0;border:none;background:none;color:#58a6ff;cursor:pointer;font-size:inherit;font-family:inherit;text-decoration:underline;text-underline-offset:2px}.doc-inline-link:hover{color:#79c0ff}.doc-inline-link--broken{color:#f85149;cursor:not-allowed;text-decoration:underline wavy}.doc-inline-link--cross-project{color:#d2a8ff}.doc-inline-link--cross-project:hover{color:#e2c0ff}.doc-content h1{font-size:2em;margin:.67em 0;border-bottom:1px solid #21262d;padding-bottom:.3em}.doc-content h2{font-size:1.5em;margin:.83em 0;border-bottom:1px solid #21262d;padding-bottom:.3em}.doc-content h3{font-size:1.25em;margin:1em 0}.doc-content h4{font-size:1em;margin:1.33em 0}.doc-content p{margin:1em 0;line-height:1.6}.doc-content code{background:#161b22;padding:2px 6px;border-radius:4px;font-family:Fira Code,Cascadia Code,monospace;font-size:.9em;font-variant-ligatures:none}.doc-content pre{background:#161b22;padding:16px;border-radius:8px;overflow-x:auto;border:1px solid #30363d}.doc-content pre code{background:none;padding:0}.doc-content ul,.doc-content ol{padding-left:2em;margin:1em 0}.doc-content li{margin:.25em 0;line-height:1.6}.doc-content table{border-collapse:collapse;width:100%;margin:1em 0}.doc-content th,.doc-content td{border:1px solid #30363d;padding:8px 12px;text-align:left}.doc-content th{background:#161b22;font-weight:600}.doc-content a{color:#58a6ff;text-decoration:none}.doc-content a:hover{text-decoration:underline}.doc-content blockquote{border-left:3px solid #30363d;padding-left:16px;margin:1em 0;color:#8b949e}.search-bar{display:flex;align-items:center;gap:8px}.search-scope-toggle{display:flex;align-items:center;gap:6px;cursor:pointer;-webkit-user-select:none;user-select:none}.search-scope-label{font-size:11px;color:#8b949e;white-space:nowrap}.toggle-switch{position:relative;display:inline-block;width:32px;height:18px;flex-shrink:0}.toggle-switch input{opacity:0;width:0;height:0}.toggle-slider{position:absolute;cursor:pointer;inset:0;background:#30363d;border-radius:18px;transition:background .2s}.toggle-slider:before{content:"";position:absolute;width:14px;height:14px;left:2px;bottom:2px;background:#8b949e;border-radius:50%;transition:transform .2s,background .2s}.toggle-switch input:checked+.toggle-slider{background:#1f6feb}.toggle-switch input:checked+.toggle-slider:before{background:#fff;transform:translate(14px)}.search-input{flex:1;padding:6px 12px;border:1px solid #30363d;background:#0d1117;color:#c9d1d9;border-radius:6px;font-size:13px;outline:none;transition:border-color .15s}.search-input:focus{border-color:#1f6feb}.search-input::placeholder{color:#484f58}.search-btn,.clear-btn{padding:6px 12px;border:1px solid #30363d;background:#21262d;color:#c9d1d9;border-radius:6px;cursor:pointer;font-size:12px;white-space:nowrap}.search-btn:hover,.clear-btn:hover{background:#30363d}.search-results{position:absolute;top:100%;left:0;right:0;background:#161b22;border:1px solid #30363d;border-radius:0 0 6px 6px;max-height:300px;overflow-y:auto;z-index:100}.search-result-item{padding:10px 12px;cursor:pointer;border-bottom:1px solid #21262d}.search-result-item:hover{background:#1c2129}.search-result-title{font-size:13px;font-weight:500;color:#f0f6fc}.search-result-score{font-size:11px;color:#3fb950;margin-left:8px}.search-result-excerpt{font-size:11px;color:#8b949e;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.project-selector{display:flex;align-items:center;gap:4px}.project-select{padding:4px 8px;border:1px solid #30363d;background:#0d1117;color:#c9d1d9;border-radius:6px;font-size:12px;outline:none}.new-project-input{padding:4px 8px;border:1px solid #30363d;background:#0d1117;color:#c9d1d9;border-radius:6px;font-size:12px;width:100px;outline:none}.new-project-btn{padding:4px 8px;border:1px solid #30363d;background:#21262d;color:#c9d1d9;border-radius:6px;cursor:pointer;font-size:12px}.settings-overlay{position:fixed;inset:0;background:#00000080;z-index:200;display:flex;align-items:center;justify-content:center}.settings-popover{background:#161b22;border:1px solid #30363d;border-radius:12px;padding:24px;width:400px;max-width:90%}.settings-title{font-size:16px;font-weight:600;margin:0 0 16px;color:#f0f6fc}.settings-field{margin-bottom:12px}.settings-label{display:block;font-size:12px;color:#8b949e;margin-bottom:4px}.settings-input{width:100%;padding:6px 10px;border:1px solid #30363d;background:#0d1117;color:#c9d1d9;border-radius:6px;font-size:13px;outline:none;box-sizing:border-box}.settings-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}.settings-save-btn{padding:6px 16px;border:none;background:#1f6feb;color:#fff;border-radius:6px;cursor:pointer;font-size:13px}.settings-cancel-btn{padding:6px 16px;border:1px solid #30363d;background:#21262d;color:#c9d1d9;border-radius:6px;cursor:pointer;font-size:13px}.diff-toggle-btn{padding:3px 10px;border:1px solid #d29922;background:#3d2a0033;color:#d29922;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600;transition:all .15s;white-space:nowrap;flex-shrink:0}.diff-toggle-btn:hover:not(:disabled){background:#3d2a0088}.diff-toggle-btn.active{background:#1a3a2a44;border-color:#3fb950;color:#3fb950}.diff-toggle-btn.active:hover{background:#1a3a2a88}.diff-toggle-btn:disabled{opacity:.5;cursor:default}.code-block-wrapper{border:1px solid #30363d;border-radius:6px;margin:12px 0;overflow:hidden}.code-block-header{display:flex;align-items:center;justify-content:space-between;padding:4px 12px;background:#21262d;border-bottom:1px solid #30363d}.code-block-lang-select{background:transparent;border:none;color:#8b949e;font-size:11px;font-family:Fira Code,Cascadia Code,Consolas,monospace;cursor:pointer;outline:none;padding:2px 0}.code-block-lang-select:hover{color:#c9d1d9}.code-block-lang-select option{background:#21262d;color:#c9d1d9}.code-block-copy-btn{background:none;border:1px solid #30363d;color:#8b949e;font-size:11px;cursor:pointer;padding:2px 8px;border-radius:4px;transition:all .15s}.code-block-copy-btn:hover{background:#30363d;color:#c9d1d9}.diff-viewer{flex:1;overflow:auto;background:#0d1117;border:1px solid #30363d;border-radius:8px;margin-top:4px}.diff-pre{margin:0;padding:0;font-family:Fira Code,Cascadia Code,Consolas,monospace;font-size:12.5px;line-height:1.55;white-space:pre;overflow:visible}.diff-line{display:block;padding:0 16px;min-height:1.55em}.diff-line--added{background:#1a3a1a;color:#7ee787}.diff-line--removed{background:#3a1a1a;color:#f47067}.diff-line--hunk{background:#0d1b2a;color:#79c0ff;font-weight:600}.diff-line--file-header{background:#161b22;color:#8b949e;font-style:italic}.diff-line--section-title{background:#1c1c2a;color:#c9d1d9;font-weight:700;padding-top:12px;padding-bottom:4px;border-top:1px solid #30363d;margin-top:4px}.diff-line--separator{background:#0f1117;color:#30363d;border-top:1px solid #21262d;border-bottom:1px solid #21262d}.diff-line--context{color:#c9d1d9}.file-status-table{width:100%;border-collapse:collapse;margin-top:12px}.file-status-table th,.file-status-table td{border:1px solid #30363d;padding:6px 10px;font-size:12px;text-align:left}.file-status-table th{background:#161b22;color:#8b949e}.status-ok{color:#3fb950}.status-modified{color:#d29922}.status-missing{color:#f85149}.history-viewer{display:flex;height:calc(100vh - 120px);gap:24px;overflow:hidden}.history-list{width:320px;flex-shrink:0;overflow-y:auto;padding-right:12px;border-right:1px solid #30363d}.history-entry{display:flex;flex-direction:column;width:100%;padding:12px;background:#161b22;border:1px solid #30363d;border-radius:6px;margin-bottom:8px;cursor:pointer;transition:all .15s;text-align:left;gap:6px}.history-entry:hover{border-color:#8b949e;background:#1c2129}.history-entry.history-entry--active{border-color:#1f6feb;background:#1f6feb11;box-shadow:0 0 0 1px #1f6feb}.history-entry-header{display:flex;align-items:center;justify-content:space-between;width:100%}.history-entry-version{font-size:13px;font-weight:600;color:#f0f6fc}.history-entry-type{font-size:10px;padding:2px 8px;border-radius:10px;font-weight:600;text-transform:uppercase;width:fit-content}.history-entry-type--create{background:#1a3a2a;color:#3fb950}.history-entry-type--update{background:#1c2a3a;color:#58a6ff}.history-entry-type--patch{background:#3d2a00;color:#d29922}.history-entry-type--delete{background:#3a1a1a;color:#f85149}.history-entry-type--snapshot{background:#1e1a3a;color:#a371f7}.history-group{display:flex;flex-direction:column}.history-entry--snapshot-card{padding:0}.history-entry-snapshot-main{display:flex;flex-direction:column;gap:4px;width:100%;padding:10px 12px;background:none;border:none;cursor:pointer;text-align:left;color:inherit}.history-entry-footer{display:flex;align-items:center;justify-content:space-between;gap:8px}.history-group-toggle{font-size:11px;color:#8b949e;cursor:pointer;white-space:nowrap;padding:2px 6px;border-radius:4px;transition:color .15s,background .15s;-webkit-user-select:none;user-select:none}.history-group-toggle:hover{color:#a371f7;background:#a371f715}.history-group-toggle--open{color:#a371f7}.history-group-entries{display:flex;flex-direction:column;margin-left:12px;border-left:2px solid #a371f730;padding-left:4px;margin-bottom:6px}.history-entry--sub{font-size:12px;padding:7px 10px}.history-entry--sub .history-entry-version{font-size:11px;color:#8b949e}.history-uncommitted{display:flex;flex-direction:column;margin-bottom:8px;border:1px dashed #30363d;border-radius:6px;overflow:hidden}.history-uncommitted-label{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:#8b949e;padding:5px 10px;background:#161b22;border-bottom:1px dashed #30363d}.history-entry-date{font-size:11px;color:#8b949e}.history-content{flex:1;overflow-y:auto;padding-bottom:40px}.history-content-meta{display:flex;flex-direction:column;gap:12px;padding:16px;background:#161b22;border:1px solid #30363d;border-radius:8px;margin-bottom:24px}.history-content-meta-title{display:flex;align-items:center;justify-content:space-between;width:100%}.history-content-meta-info{display:flex;align-items:center;gap:12px}.history-content-meta strong{font-size:18px;color:#f0f6fc}.history-content-version{background:#21262d;color:#8b949e;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600}.history-content-date{font-size:13px;color:#8b949e}.history-diff-loading{font-size:12px;color:#8b949e;font-style:italic}.history-diff-base{font-size:11px;color:#8b949e;background:#21262d;border:1px solid #30363d;border-radius:4px;padding:2px 7px;font-family:monospace}.history-diff-recap-banner{font-size:12px;color:#8b949e;background:#161b22;border:1px solid #30363d;border-left:3px solid #388bfd;border-radius:4px;padding:7px 12px;margin-bottom:12px}.history-meta-diff{display:flex;flex-direction:column;gap:8px;margin-top:12px;padding:12px 14px;background:#0d1117;border:1px solid #30363d;border-radius:6px}.history-meta-diff-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:#8b949e;margin-bottom:2px}.history-meta-diff-row{display:flex;align-items:center;flex-wrap:wrap;gap:6px}.history-meta-diff-field{font-size:12px;font-weight:600;color:#8b949e;min-width:90px}.history-meta-diff-arrow{font-size:12px;color:#8b949e}.history-meta-diff-chips{display:flex;flex-wrap:wrap;gap:4px}.history-meta-diff-chip--added{font-size:12px;padding:2px 8px;border-radius:4px;background:#3fb95026;color:#3fb950;border:1px solid rgba(63,185,80,.3)}.history-meta-diff-chip--removed{font-size:12px;padding:2px 8px;border-radius:4px;background:#f8514926;color:#f85149;border:1px solid rgba(248,81,73,.3);text-decoration:line-through}.history-inline-diff{font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:13.5px;line-height:1.8;white-space:pre-wrap;word-break:break-word;color:#e6edf3;padding:16px}.history-inline-diff--added{background:#3fb95040;color:#3fb950;border-radius:2px}.history-inline-diff--removed{background:#f8514940;color:#f85149;border-radius:2px;text-decoration:line-through}.doc-editor{display:flex;flex-direction:column;flex:1;min-height:0;gap:12px;margin-top:4px}.doc-editor-textarea{flex:1;min-height:400px;background:#161b22;color:#e1e4e8;border:1px solid #30363d;border-radius:6px;padding:16px;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:13px;line-height:1.6;resize:vertical;outline:none}.doc-editor-textarea:focus{border-color:#388bfd}.doc-editor-actions{display:flex;align-items:center;gap:12px;justify-content:flex-end}.doc-editor-error{color:#f85149;font-size:13px;margin-right:auto}.save-btn{background:#238636;color:#fff;border:none;border-radius:6px;padding:6px 16px;font-size:13px;cursor:pointer;transition:background .15s}.save-btn:hover:not(:disabled){background:#2ea043}.save-btn:disabled{opacity:.5;cursor:not-allowed}.edit-btn{background:none;border:1px solid #30363d;color:#8b949e;border-radius:6px;padding:4px 10px;font-size:12px;cursor:pointer;transition:all .15s}.edit-btn:hover,.edit-btn.active{background:#21262d;color:#f0f6fc;border-color:#8b949e}@media print{.dashboard-header,.sidebar-wrapper,.doc-header,.doc-related,.file-status-view{display:none!important}.dashboard,.dashboard-body,.doc-viewer{display:block!important;background:#fff!important;color:#000!important;height:auto!important;overflow:visible!important}.doc-content{max-width:100%!important;padding:0!important;color:#000!important;background:#fff!important}pre,code{background:#f5f5f5!important;color:#333!important;border:1px solid #ddd!important}}.create-doc-popover{width:min(960px,95vw);max-width:95vw;max-height:90vh;display:flex;flex-direction:column}.create-doc-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px 16px;margin-bottom:12px}.create-doc-content-field{display:flex;flex-direction:column;flex:1;min-height:0}.create-doc-textarea{flex:1;min-height:380px;width:100%;box-sizing:border-box;resize:vertical;font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,monospace;font-size:13px;line-height:1.5}.create-doc-hint{margin-top:4px;font-size:11px;color:#6e7681}.folder-combobox{position:relative}.folder-combobox .settings-input{padding-right:28px}.folder-combobox-toggle{position:absolute;top:50%;right:6px;transform:translateY(-50%);background:transparent;border:none;color:#8b949e;cursor:pointer;font-size:12px;padding:2px 6px;border-radius:4px;line-height:1}.folder-combobox-toggle:hover{color:#c9d1d9;background:#21262d}.folder-combobox-menu{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:10;background:#0d1117;border:1px solid #30363d;border-radius:6px;box-shadow:0 8px 24px #00000080;max-height:220px;overflow-y:auto;padding:4px 0}.folder-combobox-header{padding:6px 10px 4px;font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:#6e7681;border-bottom:1px solid #21262d;margin-bottom:4px}.folder-combobox-empty{padding:8px 10px;font-size:12px;color:#8b949e;font-style:italic}.folder-combobox-list{list-style:none;margin:0;padding:0}.folder-combobox-item{padding:6px 10px;font-size:13px;color:#c9d1d9;cursor:pointer;font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,monospace}.folder-combobox-item.active,.folder-combobox-item:hover{background:#1f2a3a;color:#f0f6fc}@media(max-width:720px){.create-doc-grid{grid-template-columns:1fr}.create-doc-textarea{min-height:260px}}