@specship/specship-win32-arm64 0.11.5 → 0.11.6

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.
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Deterministic intent classifier for the dashboard chat (REQ-DASH-CHAT-002).
3
+ *
4
+ * A **pure** rule-based router: given the raw message text it returns the intent
5
+ * and the query to run — **no language model, no I/O, no `cg`** (A4). The chat
6
+ * route feeds the result to a dispatcher (`answerForIntent` in
7
+ * `../routes/chat-answer.ts`) that runs the matching knowledge-base query; this
8
+ * module never touches the graph itself, so it is unit-testable in isolation.
9
+ *
10
+ * Routing order (first match wins, most specific first):
11
+ * 1. **Slash commands** (A1) — `/ss-spec <ID>` → spec, `/ss-explore …` →
12
+ * explore, `/ss-check …` → drift (the launch set has no separate
13
+ * gate/health intent, so the check family routes to drift).
14
+ * 2. **Free-form keyword/pattern** (A2 / A5) — regexes on the lowercased
15
+ * message pick callers / callees / impact / explore / drift / domain, or a
16
+ * bare spec-id token → spec.
17
+ * 3. **Fallback** (A3) — `{ intent: 'search', confident: false }`. Never null,
18
+ * never a guessed intent: an unmatched message runs full-text search.
19
+ *
20
+ * The subject/query is derived with `extractSubject` (shared with the answer
21
+ * core) so tokenization — slash-token stripping, stopword removal — is
22
+ * identical to what the search path expects.
23
+ */
24
+ import { extractSubject } from '../routes/chat-answer.js';
25
+ /** A spec-id-shaped token — `REQ-DASH-CHAT-002`, `DOMAIN-LEDGER`, `DASH-CHAT-DOC`. */
26
+ const SPEC_ID = /\b[A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)+\b/;
27
+ /**
28
+ * Intent-signalling words that survive the shared `extractSubject` stopword
29
+ * pass (which stays conservative so the generic search path doesn't drop real
30
+ * symbol names). Stripped here — inside the classifier — so a routed query like
31
+ * "impact of recordEntry" or "define Ledger" reduces to just its subject.
32
+ */
33
+ const INTENT_WORDS = new Set([
34
+ 'impact', 'define', 'definition', 'flow', 'trace',
35
+ 'drift', 'drifted', 'drifting', 'drifts',
36
+ 'caller', 'callers', 'callee', 'callees',
37
+ ]);
38
+ /** The extracted subject with intent-signalling words removed. */
39
+ function subjectOf(message) {
40
+ const { query } = extractSubject(message);
41
+ return query
42
+ .split(' ')
43
+ .filter((t) => t.length > 0 && !INTENT_WORDS.has(t.toLowerCase()))
44
+ .join(' ');
45
+ }
46
+ /**
47
+ * Classify a chat message into an intent + query. Pure function of the text
48
+ * (A4): identical input always yields identical output, and nothing here reads
49
+ * a file, a database, or a model.
50
+ */
51
+ export function classifyIntent(message) {
52
+ const msg = message.trim();
53
+ const subject = subjectOf(msg);
54
+ // --- 1. Slash commands (A1) ----------------------------------------------
55
+ const slash = msg.match(/^\/(\S+)\s*([\s\S]*)$/);
56
+ if (slash) {
57
+ const cmd = (slash[1] ?? '').toLowerCase();
58
+ const rest = (slash[2] ?? '').trim();
59
+ if (cmd === 'ss-spec' || cmd === 'spec') {
60
+ return { intent: 'spec', query: rest, subject: rest || undefined, confident: true };
61
+ }
62
+ if (cmd === 'ss-explore' || cmd === 'explore') {
63
+ return { intent: 'explore', query: rest, subject: rest || undefined, confident: true };
64
+ }
65
+ if (cmd === 'ss-check' || cmd === 'check') {
66
+ return { intent: 'drift', query: rest, subject: rest || undefined, confident: true };
67
+ }
68
+ // Unknown slash command → fall through to keyword matching on the subject
69
+ // (extractSubject has already stripped the leading slash token).
70
+ }
71
+ const lower = msg.toLowerCase();
72
+ const withSubject = (intent) => ({
73
+ intent,
74
+ query: subject,
75
+ subject: subject || undefined,
76
+ confident: true,
77
+ });
78
+ // --- 2. Free-form keyword / pattern (A2, A5) ------------------------------
79
+ // "who calls X" / "what calls X" → callers.
80
+ if (/\b(who|what)\s+calls\b/.test(lower))
81
+ return withSubject('callers');
82
+ // "what does X call" / "callees of X" → callees (checked before explore so a
83
+ // "call" phrasing never falls through to the generic how-does path).
84
+ if (/\bwhat\s+does\b[\s\S]*\bcalls?\b/.test(lower) || /\bcallees?\b/.test(lower)) {
85
+ return withSubject('callees');
86
+ }
87
+ // "what breaks if I change X" / "impact of X" → impact.
88
+ if (/\bwhat\s+breaks\b/.test(lower) || /\bimpact\s+of\b/.test(lower) || /\bif\s+i\s+change\b/.test(lower)) {
89
+ return withSubject('impact');
90
+ }
91
+ // "how does X work" / "the flow" / "trace X" → explore.
92
+ if (/\bhow\s+does\b/.test(lower) || /\bflow\b/.test(lower) || /\btrace\b/.test(lower)) {
93
+ return withSubject('explore');
94
+ }
95
+ // "what's drifted" / "drift" → drift queue (before domain so "what's drifted"
96
+ // doesn't get read as a "what is …" definition lookup).
97
+ if (/\bdrift(ed|ing|s)?\b/.test(lower))
98
+ return withSubject('drift');
99
+ // "what is <term>" / "define X" → domain lookup.
100
+ if (/\bwhat\s+is\b/.test(lower) || /\bwhat\s+are\b/.test(lower) || /\bdefine\b/.test(lower) || /\bdefinition\s+of\b/.test(lower)) {
101
+ return withSubject('domain');
102
+ }
103
+ // A bare spec-id token (e.g. `REQ-DASH-CHAT-002`) → spec lookup for that id.
104
+ const specId = msg.match(SPEC_ID);
105
+ if (specId && specId[0]) {
106
+ return { intent: 'spec', query: specId[0], subject: specId[0], confident: true };
107
+ }
108
+ // --- 3. Fallback (A3) — never null, never a guessed intent ----------------
109
+ return { intent: 'search', query: subject, subject: subject || undefined, confident: false };
110
+ }
@@ -0,0 +1,488 @@
1
+ /**
2
+ * Deterministic answer engine for the dashboard chat (REQ-DASH-CHAT-001).
3
+ *
4
+ * This is the pure, Fastify-free core the `/api/chat` route delegates to. It
5
+ * answers a free-form question **entirely from the project's own indexed data**
6
+ * — the code knowledge graph (symbols), specs and their links, and domain facts
7
+ * — by running database queries through the instance and composing prose from
8
+ * the real rows returned. It is emphatically **not** a chatbot:
9
+ *
10
+ * - **No LLM, no network (A2 / DOM-SPECSHIP-004).** Nothing here calls a
11
+ * language model or any external service. This module imports nothing
12
+ * LLM-shaped and reads no model credentials — answers are produced by
13
+ * deterministic subject extraction + queries + templated composition.
14
+ * - **Deterministic (A3).** Given the same index and question, the produced
15
+ * answer is byte-identical: every result set is sorted by a stable key
16
+ * before rendering, and there is no `Date.now()` / `Math.random()` / any
17
+ * insertion-order dependence in the output path.
18
+ * - **Traceable (A1).** Every fact rendered into the answer carries a matching
19
+ * `ChatSource` pointing at the exact row it came from (a symbol's qualified
20
+ * name, a spec id, a domain fact id).
21
+ * - **Honest (A4).** When the subject matches nothing indexed, the engine
22
+ * returns `{ found: false }` with a plain not-found sentence — it never
23
+ * invents a symbol, path, or fact.
24
+ *
25
+ * Driven only through instance query methods (`searchNodes`, `getSpecQueries()`)
26
+ * so the server never runtime-imports the `@specship/specship` package (which
27
+ * would silently serve a stale build).
28
+ */
29
+ import { readFileSync } from 'node:fs';
30
+ import { join } from 'node:path';
31
+ /**
32
+ * Per-store caps for the **prose** sections only — keep the composed gloss
33
+ * bounded and readable. The full-detail set below is a separate, larger cap.
34
+ */
35
+ const MAX_SYMBOLS = 5;
36
+ const MAX_SPECS = 3;
37
+ const MAX_DOMAIN = 3;
38
+ /**
39
+ * Cap on the ranked full-detail match set (REQ-DASH-CHAT-005.A3). The concise
40
+ * prose stays bounded by the small per-store caps above; the detail array —
41
+ * each source's verbatim body / links, rendered as one expandable per match in
42
+ * the UI — is the top matches by rank up to this cap, so a broad query can't
43
+ * return unbounded content. Raised well above the old five-per-store limit;
44
+ * every dispatch helper takes it as a parameter so it stays configurable.
45
+ */
46
+ const DETAIL_CAP = 15;
47
+ /**
48
+ * Words that carry no subject signal — dropped before building the search
49
+ * query. Deterministic, intentionally small (question-shaping words + the
50
+ * verbs the launch intents phrase themselves with). Matching is lowercase.
51
+ */
52
+ const STOPWORDS = new Set([
53
+ 'how', 'does', 'do', 'did', 'what', 'whats', 'is', 'are', 'the', 'a', 'an',
54
+ 'who', 'where', 'why', 'when', 'which', 'this', 'that', 'these', 'those',
55
+ 'of', 'to', 'in', 'on', 'for', 'and', 'or', 'i', 'we', 'you', 'it',
56
+ 'change', 'changing', 'breaks', 'break', 'work', 'works', 'working',
57
+ 'reach', 'reaches', 'become', 'becomes', 'call', 'calls', 'called',
58
+ 'about', 'with', 'from', 'into', 'me', 'my', 'show', 'tell', 'explain',
59
+ 'find', 'get', 'look', 'up', 'be', 'was', 'were', 'has', 'have', 'had',
60
+ ]);
61
+ /**
62
+ * Extract the deterministic search subject from a raw question.
63
+ *
64
+ * Strips a leading slash-command token, splits on non-identifier characters
65
+ * (keeping `_`, `.` and `:` so `Foo.bar` / `a::b` survive as one token),
66
+ * drops stopwords, and joins the remainder. Identifier-shaped tokens
67
+ * (CamelCase, snake_case, dotted, or ≥3-char words) are preferred; if none
68
+ * survive, every non-stopword token is used so a plain-English question still
69
+ * searches. Returns the joined subject plus the raw token list. Pure.
70
+ */
71
+ export function extractSubject(question) {
72
+ const withoutCommand = question.trim().replace(/^\/\S+\s*/, '');
73
+ const rawTokens = withoutCommand
74
+ .split(/[^A-Za-z0-9_.:]+/)
75
+ .map((t) => t.replace(/^[.:]+|[.:]+$/g, '')) // trim stray separators
76
+ .filter((t) => t.length > 0);
77
+ const kept = rawTokens.filter((t) => !STOPWORDS.has(t.toLowerCase()));
78
+ const identifierLike = kept.filter((t) => /[A-Z]/.test(t.slice(1)) || t.includes('_') || t.includes('.') || t.includes(':') || t.length >= 3);
79
+ const subjectTokens = identifierLike.length > 0 ? identifierLike : kept;
80
+ return { query: subjectTokens.join(' '), tokens: subjectTokens };
81
+ }
82
+ /** Stable symbol ordering: score desc, then qualified name asc (tie-break). */
83
+ function bySymbolRank(a, b) {
84
+ if (b.score !== a.score)
85
+ return b.score - a.score;
86
+ return a.node.qualifiedName < b.node.qualifiedName ? -1 : a.node.qualifiedName > b.node.qualifiedName ? 1 : 0;
87
+ }
88
+ /** Stable spec ordering: score desc, then spec id asc (tie-break). */
89
+ function bySpecRank(a, b) {
90
+ if (b.score !== a.score)
91
+ return b.score - a.score;
92
+ return a.spec.id < b.spec.id ? -1 : a.spec.id > b.spec.id ? 1 : 0;
93
+ }
94
+ /** First non-empty line of a spec/domain body, trimmed — the one-line gloss. */
95
+ function firstLine(body) {
96
+ for (const raw of body.split('\n')) {
97
+ const line = raw.replace(/^#+\s*/, '').trim();
98
+ if (line.length > 0)
99
+ return line;
100
+ }
101
+ return '';
102
+ }
103
+ // =============================================================================
104
+ // Full retrieved detail per match (REQ-DASH-CHAT-005)
105
+ // =============================================================================
106
+ /**
107
+ * Read a symbol's verbatim source body from disk — the exact `startLine..endLine`
108
+ * slice of its file (mirrors `src/mcp/tools.ts`). Real bytes, never synthesized
109
+ * (A4); returns `''` if the file can't be read so a missing file is honest, not
110
+ * fabricated.
111
+ */
112
+ function readNodeBody(cg, node) {
113
+ try {
114
+ const abs = join(cg.getProjectRoot(), node.filePath);
115
+ const lines = readFileSync(abs, 'utf-8').split('\n');
116
+ return lines.slice(node.startLine - 1, node.endLine).join('\n');
117
+ }
118
+ catch {
119
+ return '';
120
+ }
121
+ }
122
+ /** Map 1-hop neighbours to deduped, stable-sorted `SymbolRef`s (self excluded). */
123
+ function neighbourRefs(raw, selfId) {
124
+ const seen = new Set();
125
+ return raw
126
+ .filter((r) => r.node.id !== selfId && !seen.has(r.node.id) && (seen.add(r.node.id), true))
127
+ .map((r) => ({ name: r.node.name, qualifiedName: r.node.qualifiedName, filePath: r.node.filePath, line: r.node.startLine }))
128
+ .sort((a, b) => byName(a.qualifiedName, b.qualifiedName));
129
+ }
130
+ /**
131
+ * Full symbol detail (A1): verbatim body + signature + immediate callers/callees.
132
+ * Read-equivalent depth so the user never has to open the file to see the match.
133
+ */
134
+ function buildSymbolDetail(cg, node) {
135
+ return {
136
+ signature: node.signature,
137
+ body: readNodeBody(cg, node),
138
+ callers: neighbourRefs(cg.getCallers(node.id, 1), node.id),
139
+ callees: neighbourRefs(cg.getCallees(node.id, 1), node.id),
140
+ };
141
+ }
142
+ /**
143
+ * Full spec detail (A2): the spec's full body + the code it links to, each with
144
+ * its current link state. Links are stable-sorted by target (then state) so the
145
+ * detail is byte-identical across repeat calls (A4).
146
+ */
147
+ function buildSpecDetail(cg, spec) {
148
+ const links = cg
149
+ .getSpecQueries()
150
+ .getLinksBySpec(spec.id)
151
+ .map((l) => ({ target: l.targetQualifiedName, state: l.state }))
152
+ .sort((a, b) => (a.target !== b.target ? byName(a.target, b.target) : byName(a.state, b.state)));
153
+ return { body: spec.body, links };
154
+ }
155
+ /** A symbol `ChatSource` carrying its full detail (A1 + REQ-005). */
156
+ function symbolSource(cg, node) {
157
+ return {
158
+ kind: 'symbol',
159
+ ref: node.qualifiedName,
160
+ label: node.name,
161
+ filePath: node.filePath,
162
+ line: node.startLine,
163
+ detail: buildSymbolDetail(cg, node),
164
+ };
165
+ }
166
+ /**
167
+ * Answer a question deterministically from the indexed knowledge base.
168
+ *
169
+ * Runs a symbol search and a spec search (domain facts are specs of kind
170
+ * `domain`, split out into their own section), sorts every result set by a
171
+ * stable key, and composes a templated prose answer from the real rows. When
172
+ * nothing matches, returns an honest not-found answer with no sources.
173
+ */
174
+ export function answerFromKnowledgeBase(cg, question, detailCap = DETAIL_CAP) {
175
+ const { query } = extractSubject(question);
176
+ const subjectLabel = query.length > 0 ? query : question.trim();
177
+ // No usable subject → nothing to search; honest empty answer (A4).
178
+ if (query.length === 0) {
179
+ return notFound(subjectLabel);
180
+ }
181
+ // --- Ranked hits per store (wider than the prose caps to feed the detail
182
+ // set; each list is stable-sorted so repeat calls are byte-identical). --
183
+ const symbolHits = cg
184
+ .searchNodes(query, { limit: detailCap * 4 })
185
+ .slice()
186
+ .sort(bySymbolRank)
187
+ .slice(0, detailCap);
188
+ const specHits = cg
189
+ .getSpecQueries()
190
+ .searchSpecs(query, detailCap * 4)
191
+ .slice()
192
+ .sort(bySpecRank);
193
+ const domainHits = specHits.filter((r) => r.spec.kind === 'domain').slice(0, detailCap);
194
+ const nonDomainSpecHits = specHits.filter((r) => r.spec.kind !== 'domain').slice(0, detailCap);
195
+ if (symbolHits.length === 0 && domainHits.length === 0 && nonDomainSpecHits.length === 0) {
196
+ return notFound(subjectLabel);
197
+ }
198
+ // --- Concise prose: the small per-store caps, gloss only (no detail). ------
199
+ const proseSymbols = symbolHits.slice(0, MAX_SYMBOLS);
200
+ const proseSpecs = nonDomainSpecHits.slice(0, MAX_SPECS);
201
+ const proseDomain = domainHits.slice(0, MAX_DOMAIN);
202
+ const sections = [];
203
+ if (proseSymbols.length > 0) {
204
+ const parts = proseSymbols.map((r) => `\`${r.node.name}\` (${r.node.kind} in ${r.node.filePath}:${r.node.startLine})`);
205
+ const noun = proseSymbols.length === 1 ? 'symbol' : 'symbols';
206
+ sections.push(`Found ${proseSymbols.length} ${noun} matching “${subjectLabel}”: ${parts.join(', ')}.`);
207
+ }
208
+ if (proseSpecs.length > 0) {
209
+ const parts = proseSpecs.map((r) => `${r.spec.id} — ${r.spec.title}`);
210
+ const noun = proseSpecs.length === 1 ? 'spec' : 'specs';
211
+ sections.push(`Related ${noun}: ${parts.join('; ')}.`);
212
+ }
213
+ if (proseDomain.length > 0) {
214
+ const parts = proseDomain.map((r) => {
215
+ const gloss = firstLine(r.spec.body);
216
+ return gloss ? `${r.spec.title} — ${gloss}` : r.spec.title;
217
+ });
218
+ const noun = proseDomain.length === 1 ? 'domain fact' : 'domain facts';
219
+ sections.push(`Related ${noun}: ${parts.join('; ')}.`);
220
+ }
221
+ // --- Full-detail sources: prose-mentioned first (so every rendered fact has
222
+ // its source ref, A1), then extra ranked matches fill the detail budget so
223
+ // the user sees everything retrieved — bounded at detailCap (A3, A5). -----
224
+ const sources = [];
225
+ const specSource = (r) => ({
226
+ kind: 'spec', ref: r.spec.id, label: r.spec.title, detail: buildSpecDetail(cg, r.spec),
227
+ });
228
+ const domainSource = (r) => ({
229
+ kind: 'domain', ref: r.spec.id, label: r.spec.title, detail: { body: r.spec.body },
230
+ });
231
+ const add = (src) => { if (sources.length < detailCap)
232
+ sources.push(src); };
233
+ for (const r of proseSymbols)
234
+ add(symbolSource(cg, r.node));
235
+ for (const r of proseSpecs)
236
+ add(specSource(r));
237
+ for (const r of proseDomain)
238
+ add(domainSource(r));
239
+ for (const r of symbolHits.slice(MAX_SYMBOLS))
240
+ add(symbolSource(cg, r.node));
241
+ for (const r of nonDomainSpecHits.slice(MAX_SPECS))
242
+ add(specSource(r));
243
+ for (const r of domainHits.slice(MAX_DOMAIN))
244
+ add(domainSource(r));
245
+ return { found: true, answer: sections.join('\n\n'), sources };
246
+ }
247
+ // =============================================================================
248
+ // Faux-streaming chunker (REQ-DASH-CHAT-003)
249
+ // =============================================================================
250
+ /**
251
+ * Longest slice `chunkAnswer` emits. Sized to a word-group (not a whole
252
+ * sentence) so even a one-sentence answer streams in several chunks — the
253
+ * progressive-reveal "typing" effect (REQ-DASH-CHAT-003.A2) rather than one
254
+ * atomic drop.
255
+ */
256
+ const MAX_CHUNK_CHARS = 48;
257
+ /**
258
+ * Split a fully-composed answer into ordered presentation chunks.
259
+ *
260
+ * The stream route computes the whole answer first, then paces these chunks so
261
+ * the reply renders progressively like a model typing — but pacing is cosmetics
262
+ * only, so the **one invariant is `chunkAnswer(answer).join('') === answer`**
263
+ * (REQ-DASH-CHAT-003.A2/A3): chunk boundaries never add, drop, or reorder a
264
+ * character. Boundaries prefer a sentence end (`.`/`!`/`?`/`;` before a space or
265
+ * newline), then a newline, then any space, and finally a hard cut at
266
+ * `MAX_CHUNK_CHARS` so an unbroken token still streams. Pure and deterministic —
267
+ * same input always yields the same chunks, no `Date.now()`/`Math.random()`.
268
+ */
269
+ export function chunkAnswer(answer) {
270
+ const chunks = [];
271
+ const n = answer.length;
272
+ let i = 0;
273
+ while (i < n) {
274
+ const hardEnd = Math.min(i + MAX_CHUNK_CHARS, n);
275
+ if (hardEnd >= n) {
276
+ chunks.push(answer.slice(i));
277
+ break;
278
+ }
279
+ // Scan the window (i, hardEnd) for the best break, preferring a sentence
280
+ // end, then a newline, then any space. Each candidate is the index just
281
+ // AFTER the boundary char so the separator rides the current chunk.
282
+ let sentenceBreak = -1;
283
+ let spaceBreak = -1;
284
+ for (let j = i; j < hardEnd; j++) {
285
+ const c = answer[j];
286
+ const next = answer[j + 1];
287
+ if ((c === '.' || c === '!' || c === '?' || c === ';') && (next === ' ' || next === '\n')) {
288
+ sentenceBreak = j + 1;
289
+ }
290
+ else if (c === '\n') {
291
+ sentenceBreak = j + 1;
292
+ }
293
+ else if (c === ' ') {
294
+ spaceBreak = j + 1;
295
+ }
296
+ }
297
+ const end = sentenceBreak > i ? sentenceBreak : spaceBreak > i ? spaceBreak : hardEnd;
298
+ chunks.push(answer.slice(i, end));
299
+ i = end;
300
+ }
301
+ return chunks;
302
+ }
303
+ // =============================================================================
304
+ // Intent dispatcher (REQ-DASH-CHAT-002)
305
+ // =============================================================================
306
+ /**
307
+ * Route a classified message to the matching knowledge-base query.
308
+ *
309
+ * The classifier (`../chat/classify.ts`) is pure and I/O-free; this is the thin
310
+ * glue that turns its `{ intent, query }` into a concrete instance query and
311
+ * composes the deterministic answer. Each intent maps to an existing instance
312
+ * method — callers/callees/impact traverse the graph, spec/domain hit the spec
313
+ * layer, drift reads the out-of-sync links, and explore/search fall through to
314
+ * the generic knowledge-base composition. Every rendered fact still carries a
315
+ * `ChatSource` (A1) and every result set is stable-sorted (A3).
316
+ */
317
+ export function answerForIntent(cg, classified, detailCap = DETAIL_CAP) {
318
+ const query = classified.query.trim();
319
+ switch (classified.intent) {
320
+ case 'callers':
321
+ return answerNeighbours(cg, query, 'callers', detailCap);
322
+ case 'callees':
323
+ return answerNeighbours(cg, query, 'callees', detailCap);
324
+ case 'impact':
325
+ return answerImpact(cg, query, detailCap);
326
+ case 'spec':
327
+ return answerSpecs(cg, query, 'spec', detailCap);
328
+ case 'domain':
329
+ return answerSpecs(cg, query, 'domain', detailCap);
330
+ case 'drift':
331
+ return answerDrift(cg, detailCap);
332
+ case 'explore':
333
+ case 'search':
334
+ default:
335
+ return answerFromKnowledgeBase(cg, query, detailCap);
336
+ }
337
+ }
338
+ /** Stable string comparison — the tie-break every dispatch sort shares. */
339
+ function byName(a, b) {
340
+ return a < b ? -1 : a > b ? 1 : 0;
341
+ }
342
+ /** Resolve a free-form subject to its single best-matching graph node (or null). */
343
+ function resolveNode(cg, query) {
344
+ if (query.length === 0)
345
+ return null;
346
+ const hits = cg.searchNodes(query, { limit: MAX_SYMBOLS * 4 }).slice().sort(bySymbolRank);
347
+ return hits[0]?.node ?? null;
348
+ }
349
+ /** Render a symbol reference the same way across every dispatch answer. */
350
+ function symbolRef(node) {
351
+ return `\`${node.name}\` (${node.kind} in ${node.filePath}:${node.startLine})`;
352
+ }
353
+ /** callers / callees: the subject's 1-hop neighbours in the call graph. */
354
+ function answerNeighbours(cg, query, dir, detailCap = DETAIL_CAP) {
355
+ const node = resolveNode(cg, query);
356
+ if (!node)
357
+ return notFound(query);
358
+ const raw = dir === 'callers' ? cg.getCallers(node.id, 1) : cg.getCallees(node.id, 1);
359
+ const seen = new Set();
360
+ const neighbours = raw
361
+ .filter((r) => r.node.id !== node.id && !seen.has(r.node.id) && (seen.add(r.node.id), true))
362
+ .sort((a, b) => byName(a.node.qualifiedName, b.node.qualifiedName));
363
+ // Subject first, then its neighbours — each with full detail, bounded (A3).
364
+ const sources = [symbolSource(cg, node)];
365
+ for (const r of neighbours.slice(0, detailCap - 1))
366
+ sources.push(symbolSource(cg, r.node));
367
+ if (neighbours.length === 0) {
368
+ const rel = dir === 'callers' ? 'has no known callers' : 'makes no known calls';
369
+ return { found: true, answer: `${symbolRef(node)} ${rel} in the index.`, sources };
370
+ }
371
+ const shown = neighbours.slice(0, MAX_SYMBOLS);
372
+ const parts = shown.map((r) => symbolRef(r.node));
373
+ const more = neighbours.length > shown.length ? ` (+${neighbours.length - shown.length} more)` : '';
374
+ const verb = dir === 'callers' ? 'is called by' : 'calls';
375
+ const noun = dir === 'callers'
376
+ ? (neighbours.length === 1 ? 'caller' : 'callers')
377
+ : (neighbours.length === 1 ? 'callee' : 'callees');
378
+ return { found: true, answer: `\`${node.name}\` ${verb} ${neighbours.length} ${noun}${more}: ${parts.join(', ')}.`, sources };
379
+ }
380
+ /** impact: the subject's downstream blast radius. */
381
+ function answerImpact(cg, query, detailCap = DETAIL_CAP) {
382
+ const node = resolveNode(cg, query);
383
+ if (!node)
384
+ return notFound(query);
385
+ const sub = cg.getImpactRadius(node.id, 3);
386
+ const impacted = [...sub.nodes.values()]
387
+ .filter((n) => n.id !== node.id)
388
+ .sort((a, b) => byName(a.qualifiedName, b.qualifiedName));
389
+ // Subject first, then impacted symbols — each with full detail, bounded (A3).
390
+ const sources = [symbolSource(cg, node)];
391
+ for (const n of impacted.slice(0, detailCap - 1))
392
+ sources.push(symbolSource(cg, n));
393
+ if (impacted.length === 0) {
394
+ return { found: true, answer: `Changing ${symbolRef(node)} has no known downstream impact in the index.`, sources };
395
+ }
396
+ const shown = impacted.slice(0, MAX_SYMBOLS);
397
+ const parts = shown.map((n) => symbolRef(n));
398
+ const more = impacted.length > shown.length ? ` (+${impacted.length - shown.length} more)` : '';
399
+ const noun = impacted.length === 1 ? 'symbol' : 'symbols';
400
+ return { found: true, answer: `Changing \`${node.name}\` could affect ${impacted.length} ${noun}${more}: ${parts.join(', ')}.`, sources };
401
+ }
402
+ /** spec / domain: a lookup over the spec layer (exact id first for spec). */
403
+ function answerSpecs(cg, query, which, detailCap = DETAIL_CAP) {
404
+ if (query.length === 0)
405
+ return notFound(query);
406
+ const specDetailSource = (spec) => which === 'domain'
407
+ ? { kind: 'domain', ref: spec.id, label: spec.title, detail: { body: spec.body } }
408
+ : { kind: 'spec', ref: spec.id, label: spec.title, detail: buildSpecDetail(cg, spec) };
409
+ // A spec intent with an exact id resolves directly — deterministic and precise.
410
+ if (which === 'spec') {
411
+ const exact = cg.getSpecQueries().getSpecById(query);
412
+ if (exact) {
413
+ const gloss = firstLine(exact.body);
414
+ return {
415
+ found: true,
416
+ answer: gloss ? `${exact.id} — ${exact.title}: ${gloss}` : `${exact.id} — ${exact.title}`,
417
+ sources: [specDetailSource(exact)],
418
+ };
419
+ }
420
+ }
421
+ const hits = cg
422
+ .getSpecQueries()
423
+ .searchSpecs(query, detailCap * 4)
424
+ .slice()
425
+ .sort(bySpecRank);
426
+ const matched = which === 'domain'
427
+ ? hits.filter((r) => r.spec.kind === 'domain').slice(0, detailCap)
428
+ : hits.filter((r) => r.spec.kind !== 'domain').slice(0, detailCap);
429
+ if (matched.length === 0)
430
+ return notFound(query);
431
+ // Full detail for every match, bounded (A2/A3); prose lists the small cap.
432
+ const sources = matched.map((r) => specDetailSource(r.spec));
433
+ const prose = matched.slice(0, which === 'domain' ? MAX_DOMAIN : MAX_SPECS);
434
+ const parts = prose.map((r) => {
435
+ if (which === 'domain') {
436
+ const gloss = firstLine(r.spec.body);
437
+ return gloss ? `${r.spec.title} — ${gloss}` : r.spec.title;
438
+ }
439
+ return `${r.spec.id} — ${r.spec.title}`;
440
+ });
441
+ const more = matched.length > prose.length ? ` (+${matched.length - prose.length} more)` : '';
442
+ const noun = which === 'domain'
443
+ ? (matched.length === 1 ? 'domain fact' : 'domain facts')
444
+ : (matched.length === 1 ? 'spec' : 'specs');
445
+ return { found: true, answer: `Found ${matched.length} ${noun} matching “${query}”${more}: ${parts.join('; ')}.`, sources };
446
+ }
447
+ /** drift: the spec ↔ code links that are currently out of sync. */
448
+ function answerDrift(cg, detailCap = DETAIL_CAP) {
449
+ const links = cg
450
+ .getSpecQueries()
451
+ .getLinksByState(['drifted', 'broken', 'orphaned'])
452
+ .slice()
453
+ .sort((a, b) => (a.specId !== b.specId ? byName(a.specId, b.specId) : byName(a.targetQualifiedName, b.targetQualifiedName)));
454
+ if (links.length === 0) {
455
+ return {
456
+ found: true,
457
+ answer: 'No specs are currently drifted, broken, or orphaned — the spec ↔ code links are all in sync.',
458
+ sources: [],
459
+ };
460
+ }
461
+ // One source per distinct drifted spec, each carrying the spec's full detail
462
+ // (body + all its links with state), bounded at detailCap (A2/A3).
463
+ const sources = [];
464
+ const sq = cg.getSpecQueries();
465
+ const seenSpecs = new Set();
466
+ for (const l of links) {
467
+ if (sources.length >= detailCap || seenSpecs.has(l.specId))
468
+ continue;
469
+ seenSpecs.add(l.specId);
470
+ const spec = sq.getSpecById(l.specId);
471
+ sources.push(spec
472
+ ? { kind: 'spec', ref: spec.id, label: spec.title, detail: buildSpecDetail(cg, spec) }
473
+ : { kind: 'spec', ref: l.specId, label: l.specId });
474
+ }
475
+ const shown = links.slice(0, MAX_SPECS + MAX_DOMAIN);
476
+ const parts = shown.map((l) => `${l.specId} → ${l.targetQualifiedName} (${l.state})`);
477
+ const more = links.length > shown.length ? ` (+${links.length - shown.length} more)` : '';
478
+ const noun = links.length === 1 ? 'link' : 'links';
479
+ return { found: true, answer: `${links.length} spec ${noun} out of sync${more}: ${parts.join('; ')}.`, sources };
480
+ }
481
+ /** The honest not-found answer — no invented symbols, paths, or facts (A4). */
482
+ function notFound(subjectLabel) {
483
+ return {
484
+ found: false,
485
+ answer: `I couldn't find anything about “${subjectLabel}” in this project's knowledge base — no matching symbols, specs, or domain facts are indexed.`,
486
+ sources: [],
487
+ };
488
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * POST /api/chat (REQ-DASH-CHAT-001).
3
+ *
4
+ * The dashboard chat's server endpoint. It answers a free-form question
5
+ * **deterministically from the project's own indexed knowledge base** — the
6
+ * code graph, specs, and domain facts — with **no language model and no
7
+ * network call** (A2). All the answering logic lives in the pure, Fastify-free
8
+ * core (`chat-answer.ts`) so determinism / no-LLM is unit-testable in isolation;
9
+ * this file is only the HTTP shell.
10
+ *
11
+ * Project-scoped like the spec / domain / maintainability routes: chat answers
12
+ * come from one project's graph + spec layer, so the handler resolves the
13
+ * active instance and 409s when no project is selectable (mirrors
14
+ * `domain.ts`). Driven entirely through instance methods via the core, so the
15
+ * server never runtime-imports the `@specship/specship` package (which would
16
+ * silently serve a stale build).
17
+ *
18
+ * The question is routed by the deterministic, I/O-free classifier
19
+ * (`../chat/classify.ts`, REQ-DASH-CHAT-002) to the matching knowledge-base
20
+ * query via `answerForIntent`. Two shapes are served:
21
+ *
22
+ * - `POST /api/chat` — returns the fully-composed answer in one JSON response.
23
+ * - `GET /api/chat/stream` — faux-streams the SAME answer as a paced SSE
24
+ * event sequence (REQ-DASH-CHAT-003): the full answer is computed first,
25
+ * then emitted as `thinking` → `tool` → `result_summary` → `chunk`… →
26
+ * `done`. The chunking is presentation pacing only; a client disconnect ends
27
+ * the stream cleanly without throwing (A4).
28
+ */
29
+ import { answerForIntent, chunkAnswer } from './chat-answer.js';
30
+ import { classifyIntent } from '../chat/classify.js';
31
+ export async function registerChatRoutes(app) {
32
+ app.post('/api/chat', async (req, reply) => {
33
+ const cg = await app.activeCg(req);
34
+ if (!cg) {
35
+ reply.code(409).send({ error: 'no project selected', code: 'no_project' });
36
+ return;
37
+ }
38
+ const body = (req.body ?? {});
39
+ const question = typeof body.question === 'string' ? body.question.trim() : '';
40
+ if (question.length === 0) {
41
+ reply.code(400).send({ error: 'question is required', code: 'bad_request' });
42
+ return;
43
+ }
44
+ const classified = classifyIntent(question);
45
+ const { found, answer, sources } = answerForIntent(cg, classified);
46
+ return { found, answer, sources };
47
+ });
48
+ // ---------------------------------------------------------------------------
49
+ // GET /api/chat/stream (REQ-DASH-CHAT-003) — the faux-streaming variant.
50
+ //
51
+ // GET + SSE so the browser's native `EventSource` (via ApiService) drives it.
52
+ // The answer is composed in full BEFORE the first chunk is written (A3), then
53
+ // paced out as named events. Writes go straight to `reply.raw` and the handler
54
+ // returns without awaiting the pacing — mirrors `events.ts`; the setTimeout
55
+ // chain keeps writing after the handler resolves and `reply.raw.end()` closes
56
+ // the stream after the `done` sentinel. Every write is guarded so a client
57
+ // disconnect (which flips `closed` and clears the timer) ends cleanly (A4).
58
+ // ---------------------------------------------------------------------------
59
+ app.get('/api/chat/stream', async (req, reply) => {
60
+ const cg = await app.activeCg(req);
61
+ if (!cg) {
62
+ reply.code(409).send({ error: 'no project selected', code: 'no_project' });
63
+ return;
64
+ }
65
+ const q = req.query?.question;
66
+ const question = typeof q === 'string' ? q.trim() : '';
67
+ if (question.length === 0) {
68
+ reply.code(400).send({ error: 'question is required', code: 'bad_request' });
69
+ return;
70
+ }
71
+ // Compose the whole answer up front — chunk boundaries/pacing below affect
72
+ // presentation only, never content (A3).
73
+ const classified = classifyIntent(question);
74
+ const { found, answer, sources } = answerForIntent(cg, classified);
75
+ const chunks = chunkAnswer(answer);
76
+ const tool = toolForIntent(classified);
77
+ reply.raw.setHeader('Content-Type', 'text/event-stream');
78
+ reply.raw.setHeader('Cache-Control', 'no-cache');
79
+ reply.raw.setHeader('Connection', 'keep-alive');
80
+ let closed = false;
81
+ let timer;
82
+ const write = (event, data) => {
83
+ if (closed)
84
+ return;
85
+ try {
86
+ reply.raw.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
87
+ }
88
+ catch {
89
+ /* peer gone — stop writing */
90
+ }
91
+ };
92
+ // A client disconnect ends the stream cleanly: stop the pacing, write no more.
93
+ req.raw.on('close', () => {
94
+ closed = true;
95
+ if (timer)
96
+ clearTimeout(timer);
97
+ });
98
+ // Pre-chunk events, in the order the spec fixes (A1).
99
+ write('thinking', { started: true });
100
+ write('tool', { name: tool.name, input: tool.input });
101
+ write('result_summary', { found, sourceCount: sources.length });
102
+ // Pace the answer chunks, then the terminal `done` sentinel.
103
+ let idx = 0;
104
+ const step = () => {
105
+ if (closed)
106
+ return;
107
+ if (idx >= chunks.length) {
108
+ write('done', { found, sources });
109
+ if (!closed) {
110
+ try {
111
+ reply.raw.end();
112
+ }
113
+ catch { /* already closed */ }
114
+ }
115
+ return;
116
+ }
117
+ write('chunk', { text: chunks[idx] });
118
+ idx += 1;
119
+ timer = setTimeout(step, CHUNK_PACING_MS);
120
+ };
121
+ step();
122
+ });
123
+ }
124
+ /** Inter-chunk delay for the faux-stream — small, presentation-only pacing. */
125
+ const CHUNK_PACING_MS = 24;
126
+ /**
127
+ * The truthful tool-call the answer came from (REQ-DASH-CHAT-004.A2) — the
128
+ * capability name the UI renders in its tool card, plus the real query input.
129
+ * Maps each classified intent to the specship capability that answered it; the
130
+ * input is the classifier's own query so the card reflects what actually ran.
131
+ */
132
+ function toolForIntent(classified) {
133
+ const input = classified.query.trim();
134
+ switch (classified.intent) {
135
+ case 'callers': return { name: 'specship_callers', input };
136
+ case 'callees': return { name: 'specship_callees', input };
137
+ case 'impact': return { name: 'specship_impact', input };
138
+ case 'spec': return { name: 'specship_spec', input };
139
+ case 'domain': return { name: 'specship_spec', input };
140
+ case 'drift': return { name: 'specship_drifted', input };
141
+ case 'explore': return { name: 'specship_explore', input };
142
+ case 'search':
143
+ default: return { name: 'specship_search', input };
144
+ }
145
+ }
@@ -31,6 +31,7 @@ import { registerEventsRoutes } from './routes/events.js';
31
31
  import { registerReflectRoutes } from './routes/reflect.js';
32
32
  import { registerMaintainabilityRoutes } from './routes/maintainability.js';
33
33
  import { registerDomainRoutes } from './routes/domain.js';
34
+ import { registerChatRoutes } from './routes/chat.js';
34
35
  /**
35
36
  * Lazy-import the specship core. Two delivery shapes are supported:
36
37
  *
@@ -172,6 +173,7 @@ export async function createServer(options) {
172
173
  await registerReflectRoutes(app);
173
174
  await registerMaintainabilityRoutes(app);
174
175
  await registerDomainRoutes(app);
176
+ await registerChatRoutes(app);
175
177
  // Optional: serve the built Angular UI from `webDir` and fall back to
176
178
  // index.html for client-side routes. Must register AFTER the /api/*
177
179
  // routes so they take precedence over the SPA wildcard.
@@ -0,0 +1,3 @@
1
+ import{a as W}from"./chunk-JLOJCDIY.js";import{a as G}from"./chunk-YV5LB22R.js";import{b as U}from"./chunk-WBJ5VEB5.js";import{a as Q}from"./chunk-IOB4JJT4.js";import{a as K}from"./chunk-3H2EZLZN.js";import{Aa as x,Ga as k,I as O,Ia as h,Ka as s,M as v,N as y,Ra as N,Sa as H,Ta as R,Ua as B,Va as l,W as w,Wa as u,Xa as g,Ya as $,aa as z,ea as i,ka as V,lb as D,qa as T,qb as L,ra as p,sa as _,ta as F,ua as A,va as C,wa as f,xa as m,ya as o,yb as j,za as a,zb as q}from"./chunk-SYGIPC2P.js";import{a as S,b as M}from"./chunk-Q7L6LLAK.js";var te=["listEl"],ne=(t,n)=>n.name,Z=(t,n)=>n.qualifiedName,ie=(t,n)=>n.target,oe=(t,n)=>n.cmd;function ae(t,n){if(t&1&&(o(0,"div",22),x(1,"div",24),a()),t&2){let e=s().$implicit,r=s();i(),m("innerHTML",r.renderMd(e.text),z)}}function re(t,n){if(t&1&&(o(0,"div",39)(1,"div",40),l(2,"output"),a(),o(3,"div",41),l(4),a()()),t&2){let e=s().$implicit;i(4),u(e.output)}}function le(t,n){if(t&1){let e=k();o(0,"div",31)(1,"button",32),h("click",function(){let d=v(e).$implicit,c=s(3).$implicit,b=s();return y(b.toggleToolCall(c,d))}),x(2,"app-icon",33)(3,"app-icon",34),o(4,"span",35),l(5),o(6,"span",36),l(7),a()(),o(8,"span",37),x(9,"app-icon",38),l(10),a()(),p(11,re,5,1,"div",39),a()}if(t&2){let e=n.$implicit;i(),T("aria-expanded",e.open),i(),m("name",e.open?"chevronDown":"chevronRight")("size",12),i(),m("size",12),i(2),g(" ",e.name),i(2),g("(",e.input,")"),i(2),m("size",9),i(),g(" ",e.status," "),i(),_(e.open?11:-1)}}function de(t,n){if(t&1&&C(0,le,12,9,"div",31,ne),t&2){let e=s(2).$implicit;f(e.tools)}}function se(t,n){if(t&1&&l(0),t&2){let e=s(2).$implicit;g(":",e.line)}}function ce(t,n){if(t&1&&(l(0),p(1,se,1,1)),t&2){let e=s().$implicit;g(" \xB7 ",e.filePath),i(),_(e.line?1:-1)}}function pe(t,n){if(t&1&&l(0),t&2){let e=s().$implicit;g(" \xB7 ",e.ref," ")}}function _e(t,n){if(t&1&&(o(0,"div",47),l(1),a()),t&2){let e=s(3).$implicit;i(),u(e.detail.signature)}}function me(t,n){if(t&1&&(o(0,"pre",48),l(1),a()),t&2){let e=s(3).$implicit;i(),u(e.detail.body)}}function xe(t,n){if(t&1&&(o(0,"div",51),l(1),o(2,"span",36),l(3),a()()),t&2){let e=n.$implicit;i(),g("",e.name," "),i(2),$("",e.filePath,":",e.line)}}function ue(t,n){if(t&1&&(o(0,"div",49)(1,"div",50),l(2,"callers"),a(),C(3,xe,4,3,"div",51,Z),a()),t&2){let e=s(3).$implicit;i(3),f(e.detail.callers)}}function ge(t,n){if(t&1&&(o(0,"div",51),l(1),o(2,"span",36),l(3),a()()),t&2){let e=n.$implicit;i(),g("",e.name," "),i(2),$("",e.filePath,":",e.line)}}function he(t,n){if(t&1&&(o(0,"div",49)(1,"div",50),l(2,"callees"),a(),C(3,ge,4,3,"div",51,Z),a()),t&2){let e=s(3).$implicit;i(3),f(e.detail.callees)}}function Ce(t,n){if(t&1&&(p(0,_e,2,1,"div",47),p(1,me,2,1,"pre",48),p(2,ue,5,0,"div",49),p(3,he,5,0,"div",49)),t&2){let e=s(2).$implicit;_(e.detail!=null&&e.detail.signature?0:-1),i(),_(e.detail!=null&&e.detail.body?1:-1),i(),_(!(e.detail==null||e.detail.callers==null)&&e.detail.callers.length?2:-1),i(),_(!(e.detail==null||e.detail.callees==null)&&e.detail.callees.length?3:-1)}}function fe(t,n){if(t&1&&(o(0,"pre",48),l(1),a()),t&2){let e=s(3).$implicit;i(),u(e.detail.body)}}function be(t,n){if(t&1&&(o(0,"div",52)(1,"span",53),l(2),a(),x(3,"app-state-pill",54),a()),t&2){let e=n.$implicit;i(2),u(e.target),i(),m("state",e.state)}}function ve(t,n){if(t&1&&(o(0,"div",49)(1,"div",50),l(2,"links"),a(),C(3,be,4,2,"div",52,ie),a()),t&2){let e=s(3).$implicit;i(3),f(e.detail.links)}}function ye(t,n){if(t&1&&(p(0,fe,2,1,"pre",48),p(1,ve,5,0,"div",49)),t&2){let e=s(2).$implicit;_(e.detail!=null&&e.detail.body?0:-1),i(),_(!(e.detail==null||e.detail.links==null)&&e.detail.links.length?1:-1)}}function we(t,n){if(t&1&&(o(0,"pre",48),l(1),a()),t&2){let e=s(3).$implicit;i(),u(e.detail.body)}}function ke(t,n){if(t&1&&p(0,we,2,1,"pre",48),t&2){let e=s(2).$implicit;_(e.detail!=null&&e.detail.body?0:-1)}}function Se(t,n){if(t&1&&(o(0,"div",46),p(1,Ce,4,4)(2,ye,2,2)(3,ke,1,1),a()),t&2){let e=s().$implicit;i(),_(e.kind==="symbol"?1:e.kind==="spec"?2:3)}}function Me(t,n){if(t&1){let e=k();o(0,"div",43)(1,"button",44),h("click",function(){let d=v(e).$index,c=s(3).$index,b=s();return y(b.toggleDetail(c,d))}),x(2,"app-icon",33),o(3,"span",35),l(4),o(5,"span",36),p(6,ce,2,2)(7,pe,1,1),a()(),o(8,"span",45),l(9),a()(),p(10,Se,4,1,"div",46),a()}if(t&2){let e=n.$implicit,r=n.$index,d=s(3).$index,c=s();i(),T("aria-expanded",c.isDetailOpen(d,r)),i(),m("name",c.isDetailOpen(d,r)?"chevronDown":"chevronRight")("size",12),i(2),g(" ",e.label),i(2),_(e.filePath?6:7),i(3),u(e.kind),i(),_(c.isDetailOpen(d,r)?10:-1)}}function Te(t,n){if(t&1&&(o(0,"div",30)(1,"div",42),l(2),a(),C(3,Me,11,7,"div",43,F),a()),t&2){let e=s(2).$implicit;i(2),g("Retrieved detail \xB7 ",e.sources.length),i(),f(e.sources)}}function Ee(t,n){if(t&1&&(o(0,"div",23)(1,"div",25),x(2,"app-logo-mark",26),o(3,"span",27),l(4,"specship"),a()(),o(5,"div",28),x(6,"div",29),p(7,de,2,0),p(8,Te,5,1,"div",30),a()()),t&2){let e=s().$implicit,r=s();i(2),m("size",22)("glow",!1),i(4),m("innerHTML",r.renderMd(e.text),z),i(),_(e.tools!=null&&e.tools.length?7:-1),i(),_(e.sources!=null&&e.sources.length?8:-1)}}function Pe(t,n){if(t&1&&p(0,ae,2,1,"div",22)(1,Ee,9,5,"div",23),t&2){let e=n.$implicit;_(e.role==="user"?0:1)}}function Oe(t,n){t&1&&(o(0,"div",5),x(1,"span",55),l(2," thinking with specship context\u2026 "),a())}function ze(t,n){if(t&1&&(o(0,"span",59),l(1),a()),t&2){let e=s().$implicit;i(),u(e.arg)}}function Fe(t,n){if(t&1){let e=k();o(0,"button",57),h("click",function(){let d=v(e).$implicit,c=s(2);return y(c.pickSlash(d.cmd))}),o(1,"span",58),l(2),a(),p(3,ze,2,1,"span",59),o(4,"span",60),l(5),a()()}if(t&2){let e=n.$implicit;i(2),u(e.cmd),i(),_(e.arg?3:-1),i(2),u(e.desc)}}function $e(t,n){if(t&1&&(o(0,"div",8),C(1,Fe,6,3,"button",56,oe),a()),t&2){let e=s();i(),f(e.filteredSlash())}}function De(t,n){if(t&1){let e=k();o(0,"div",20)(1,"button",61),h("click",function(){v(e);let d=s();return y(d.toggleContext())}),x(2,"app-icon",62),a()()}t&2&&(i(2),m("size",14))}function Ie(t,n){if(t&1){let e=k();o(0,"div",77)(1,"button",81),h("click",function(){let d=v(e).$implicit,c=s(2);return y(c.toggleTool(d))}),x(2,"span",82),a(),o(3,"span",83),l(4),a()()}if(t&2){let e=n.$implicit,r=s(2);i(),B("on",r.enabledTools().has(e)),T("aria-pressed",r.enabledTools().has(e))("aria-label",e+(r.enabledTools().has(e)?" enabled":" disabled")),i(2),R("color",r.enabledTools().has(e)?"var(--text-primary)":"var(--text-muted)"),i(),u(e)}}function Ve(t,n){if(t&1){let e=k();o(0,"aside",21)(1,"div",63)(2,"span",64),l(3,"Context"),a(),o(4,"button",65),h("click",function(){v(e);let d=s();return y(d.toggleContext())}),x(5,"app-icon",66),a()(),o(6,"div",67)(7,"div",68),x(8,"app-icon",69),o(9,"div",70)(10,"div",71),l(11,"Project"),a(),o(12,"div",72),l(13,"~/dev/specship"),a()()(),o(14,"div",68),x(15,"app-icon",73),o(16,"div",70)(17,"div",71),l(18,"Indexed files"),a(),o(19,"div",74),l(20),a()()(),o(21,"div",68),x(22,"app-icon",75),o(23,"div",70)(24,"div",71),l(25,"Drift queue"),a(),o(26,"div",74),l(27,"\u2013 links"),a()()(),o(28,"div",76),l(29,"MCP tools"),a(),C(30,Ie,5,7,"div",77,A),a(),o(32,"div",78)(33,"div",79),l(34,"Tool access"),a(),o(35,"app-segmented",80),h("change",function(d){v(e);let c=s();return y(c.setToolAccess(d))}),a()()()}if(t&2){let e=s();i(5),m("size",13),i(3),m("size",13),i(7),m("size",13),i(5),u(e.api.isConfigured?"\u2013":"offline"),i(2),m("size",13),i(8),f(e.mcpTools),i(5),m("value",e.toolAccess())("options",e.toolAccessOptions)}}var J=[{cmd:"/ss-spec",arg:"REQ-ID | new <desc>",desc:"Intent door \u2014 view, author, implement, or review a spec"},{cmd:"/ss-explore",arg:"symbols | from\u2192to",desc:"Reads door \u2014 explore an area, trace a flow, or get impact"},{cmd:"/ss-check",arg:"(gate) | drifted | health",desc:"Gate & health door \u2014 run the gate, review drift, or see health"}],X=["specship_explore","specship_search","specship_spec","specship_link_verify"],Y=class t{sanitizer=O(j);api=O(K);listEl=L("listEl");closeStream;messages=w([{role:"assistant",text:`Hi \u2014 ask anything about your specs, code, or recent Claude Code sessions.
2
+
3
+ Try **/ss-spec REQ-AUTH-005** to inspect a requirement, or **/ss-explore validateSession** to walk its callers.`}]);draft=w("");thinking=w(!1);contextCollapsed=w(!1);enabledTools=w(new Set(X));toolAccess=w("safe");slashCommands=J;mcpTools=X;toolAccessOptions=[{value:"ask",label:"Ask"},{value:"safe",label:"Auto-safe"},{value:"all",label:"All"}];showSlash=D(()=>{let n=this.draft();return n.startsWith("/")&&!n.includes(" ")});filteredSlash=D(()=>{let n=this.draft();return J.filter(e=>e.cmd.startsWith(n))});setDraft(n){this.draft.set(n)}onKey(n){n.key==="Enter"&&!n.shiftKey&&(n.preventDefault(),this.send())}send(){let n=this.draft().trim();if(!n)return;this.draft.set(""),this.messages.update(d=>[...d,{role:"user",text:n}]),this.thinking.set(!0),this.scrollToBottom();let e=this.messages().length;this.messages.update(d=>[...d,{role:"assistant",text:"",tools:[]}]),this.closeStream?.();let r="/api/chat/stream?question="+encodeURIComponent(n);this.closeStream=this.api.openEventStream(r,(d,c)=>this.onStreamEvent(e,d,c),()=>this.endStream(),["thinking","tool","result_summary","chunk","done"])}onStreamEvent(n,e,r){switch(e){case"tool":{let d=r??{};this.patchMsg(n,c=>M(S({},c),{tools:[...c.tools??[],{name:d.name??"",input:d.input??"",output:"",status:"ok"}]}));break}case"result_summary":{let d=r??{},c=d.sourceCount??0,b=d.found?`${c} source${c===1?"":"s"} returned`:"no matches in the index";this.patchMsg(n,E=>{let P=E.tools??[];return P.length===0?E:M(S({},E),{tools:P.map((I,ee)=>ee===P.length-1?M(S({},I),{output:b}):I)})});break}case"chunk":{let c=(r??{}).text??"";this.patchMsg(n,b=>M(S({},b),{text:b.text+c})),this.scrollToBottom();break}case"done":{let d=r??{};d.sources?.length&&this.patchMsg(n,c=>M(S({},c),{sources:d.sources})),this.endStream();break}}}patchMsg(n,e){this.messages.update(r=>r.map((d,c)=>c===n?e(d):d))}endStream(){this.thinking.set(!1),this.closeStream?.(),this.closeStream=void 0,this.scrollToBottom()}ngOnDestroy(){this.closeStream?.(),this.closeStream=void 0}pickSlash(n){this.draft.set(n+" ")}toggleTool(n){this.enabledTools.update(e=>{let r=new Set(e);return r.has(n)?r.delete(n):r.add(n),r})}setToolAccess(n){this.toolAccess.set(n)}toggleContext(){this.contextCollapsed.update(n=>!n)}toggleToolCall(n,e){e.open=!e.open,this.messages.update(r=>[...r])}openDetails=w(new Set);detailKey(n,e){return`${n}:${e}`}isDetailOpen(n,e){return this.openDetails().has(this.detailKey(n,e))}toggleDetail(n,e){let r=this.detailKey(n,e);this.openDetails.update(d=>{let c=new Set(d);return c.has(r)?c.delete(r):c.add(r),c})}renderMd(n){let e=n.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/\*\*(.+?)\*\*/g,'<strong style="color:var(--text-primary)">$1</strong>').replace(/`(.+?)`/g,'<code class="mono" style="font-size:12px;background:var(--bg-canvas);padding:1px 5px;border-radius:4px;border:1px solid var(--border-subtle);color:var(--node-spec)">$1</code>').replace(/\n\n/g,"<br/><br/>").replace(/\n/g,"<br/>");return this.sanitizer.bypassSecurityTrustHtml(e)}scrollToBottom(){setTimeout(()=>{let n=this.listEl()?.nativeElement;n&&(n.scrollTop=n.scrollHeight)},0)}static \u0275fac=function(e){return new(e||t)};static \u0275cmp=V({type:t,selectors:[["app-chat"]],viewQuery:function(e,r){e&1&&N(r.listEl,te,5),e&2&&H()},decls:29,vars:8,consts:[["listEl",""],[1,"chat-page"],[1,"chat-col"],[1,"thread","scroll-y"],[1,"thread-inner"],[1,"row","gap-8","muted","thinking"],[1,"composer"],[1,"composer-inner"],["role","listbox","aria-label","Slash commands",1,"slash-menu"],[1,"composer-box","row"],["type","button","title","Attach files or spec IDs",1,"btn","btn-ghost","btn-sm","attach-btn"],["name","paperclip",3,"size"],["placeholder","Ask with specship context\u2026 / for commands, Tab to complete spec IDs","rows","1",1,"composer-ta",3,"input","keydown","value"],["type","button","aria-label","Send message",1,"btn","btn-primary","btn-sm","send-btn",3,"click","disabled"],["name","send",3,"size"],[1,"composer-footer","row","gap-8"],[1,"row","gap-6"],["name","bot",3,"size"],[1,"muted",2,"font-size","10.5px"],[1,"grow"],[1,"ctx-collapsed"],["aria-label","Context panel",1,"ctx-panel","col"],[1,"msg-user"],[1,"msg-assistant"],[1,"bubble-user",3,"innerHTML"],[1,"row","gap-8","asst-meta"],["aria-hidden","true",3,"size","glow"],[2,"font-size","12px","font-weight","600"],[1,"asst-body"],[1,"bubble-asst",3,"innerHTML"],[1,"detail-list"],[1,"tool-call"],["type","button",1,"tool-header","row","gap-8",3,"click"],[2,"color","var(--text-muted)",3,"name","size"],["name","wrench",2,"color","var(--node-code)",3,"size"],[1,"mono","grow",2,"font-size","11.5px"],[1,"muted"],[1,"pill",2,"font-size","9.5px","color","var(--success)","background","var(--success-soft)"],["name","check",3,"size"],[1,"tool-output"],[1,"mono","muted",2,"font-size","10.5px","margin-bottom","3px"],[1,"mono",2,"font-size","11.5px","color","var(--text-secondary)"],[1,"eyebrow",2,"margin","4px 0 2px"],[1,"source-detail"],["type","button",1,"detail-header","row","gap-8",3,"click"],[1,"pill",2,"font-size","9.5px","color","var(--text-muted)","background","var(--bg-canvas)"],[1,"detail-body"],[1,"mono","detail-sig"],[1,"mono","detail-src"],[1,"detail-rel"],[1,"mono","muted","detail-rel-label"],[1,"mono","detail-rel-item"],[1,"row","gap-8","detail-rel-item"],[1,"mono","grow"],[3,"state"],[1,"thinking-dot"],["type","button","role","option",1,"slash-item","row","gap-8"],["type","button","role","option",1,"slash-item","row","gap-8",3,"click"],[1,"mono",2,"font-size","12px","color","var(--accent)"],[1,"mono","muted",2,"font-size","11px"],[1,"muted","grow",2,"font-size","11px","text-align","right"],["type","button","title","Show context",1,"btn","btn-ghost","btn-xs",3,"click"],["name","chevronLeft",3,"size"],[1,"ctx-header","row"],[1,"eyebrow","grow"],["type","button",1,"btn","btn-ghost","btn-xs",3,"click"],["name","chevronRight",3,"size"],[1,"ctx-body","scroll-y"],[1,"ctx-row","row","gap-8"],["name","folder",2,"color","var(--text-muted)","flex-shrink","0",3,"size"],[2,"min-width","0"],[1,"muted",2,"font-size","10px"],[1,"mono",2,"font-size","11.5px","overflow","hidden","text-overflow","ellipsis","white-space","nowrap"],["name","box",2,"color","var(--text-muted)","flex-shrink","0",3,"size"],[1,"mono",2,"font-size","11.5px"],["name","drift",2,"color","var(--warn)","flex-shrink","0",3,"size"],[1,"eyebrow",2,"margin","16px 0 8px"],[1,"row","gap-8",2,"padding","5px 0"],[1,"ctx-footer"],[1,"eyebrow",2,"margin-bottom","6px"],["size","sm",3,"change","value","options"],["type","button",1,"toggle-btn",3,"click"],[1,"toggle-thumb"],[1,"mono",2,"font-size","11px"]],template:function(e,r){e&1&&(o(0,"div",1)(1,"div",2)(2,"div",3,0)(4,"div",4),C(5,Pe,2,1,null,null,F),p(7,Oe,3,0,"div",5),a()(),o(8,"div",6)(9,"div",7),p(10,$e,3,0,"div",8),o(11,"div",9)(12,"button",10),x(13,"app-icon",11),a(),o(14,"textarea",12),h("input",function(c){return r.setDraft(c.target.value)})("keydown",function(c){return r.onKey(c)}),a(),o(15,"button",13),h("click",function(){return r.send()}),x(16,"app-icon",14),a()(),o(17,"div",15)(18,"div",16)(19,"app-pill"),x(20,"app-icon",17),l(21," Opus 4 "),a(),o(22,"span",18),l(23,"single thread \xB7 this project"),a()(),x(24,"span",19),o(25,"span",18),l(26,"Enter to send \xB7 Shift+Enter for newline"),a()()()()(),p(27,De,3,1,"div",20)(28,Ve,36,7,"aside",21),a()),e&2&&(i(5),f(r.messages()),i(2),_(r.thinking()?7:-1),i(3),_(r.showSlash()&&r.filteredSlash().length>0?10:-1),i(3),m("size",15),i(),m("value",r.draft()),i(),m("disabled",!r.draft().trim()),i(),m("size",14),i(4),m("size",10),i(7),_(r.contextCollapsed()?27:28))},dependencies:[q,W,Q,U,G],styles:["[_nghost-%COMP%]{display:flex;flex:1;min-height:0}.chat-page[_ngcontent-%COMP%]{display:flex;flex:1;min-height:0;min-width:0}.chat-col[_ngcontent-%COMP%]{display:flex;flex-direction:column;flex:1;min-width:0;min-height:0}.thread[_ngcontent-%COMP%]{flex:1;padding:20px 24px;overflow-y:auto}.thread-inner[_ngcontent-%COMP%]{max-width:760px;margin:0 auto}.msg-user[_ngcontent-%COMP%]{display:flex;justify-content:flex-end;margin-bottom:18px}.bubble-user[_ngcontent-%COMP%]{max-width:76%;background:var(--bg-elevated);border:1px solid var(--border-subtle);border-radius:12px 12px 4px;padding:9px 13px;font-size:13.5px;line-height:1.55}.msg-assistant[_ngcontent-%COMP%]{margin-bottom:22px}.asst-meta[_ngcontent-%COMP%]{align-items:center;margin-bottom:8px}.asst-logo[_ngcontent-%COMP%]{display:inline-grid;place-items:center;width:22px;height:22px;border-radius:50%;background:var(--accent);color:#fff;font-size:11px;font-weight:700;flex-shrink:0}.asst-body[_ngcontent-%COMP%]{padding-left:30px}.bubble-asst[_ngcontent-%COMP%]{font-size:13.5px;line-height:1.62;color:var(--text-secondary);text-wrap:pretty;margin-bottom:4px}.tool-call[_ngcontent-%COMP%]{border:1px solid var(--border-subtle);border-radius:7px;margin-top:8px;overflow:hidden;background:var(--bg-canvas)}.tool-header[_ngcontent-%COMP%]{width:100%;background:transparent;border:none;cursor:pointer;padding:6px 10px;text-align:left;color:inherit;font-family:var(--font-ui)}.tool-header[_ngcontent-%COMP%]:hover{background:var(--bg-hover)}.tool-header[_ngcontent-%COMP%]:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.tool-output[_ngcontent-%COMP%]{padding:8px 10px 10px 30px;border-top:1px solid var(--border-subtle)}.detail-list[_ngcontent-%COMP%]{margin-top:10px;display:flex;flex-direction:column;gap:6px}.source-detail[_ngcontent-%COMP%]{border:1px solid var(--border-subtle);border-radius:7px;overflow:hidden;background:var(--bg-canvas)}.detail-header[_ngcontent-%COMP%]{width:100%;background:transparent;border:none;cursor:pointer;padding:6px 10px;text-align:left;color:inherit;font-family:var(--font-ui)}.detail-header[_ngcontent-%COMP%]:hover{background:var(--bg-hover)}.detail-header[_ngcontent-%COMP%]:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.detail-body[_ngcontent-%COMP%]{padding:8px 10px 10px;border-top:1px solid var(--border-subtle);display:flex;flex-direction:column;gap:8px}.detail-sig[_ngcontent-%COMP%]{font-size:11.5px;color:var(--node-code);white-space:pre-wrap;word-break:break-word}.detail-src[_ngcontent-%COMP%]{margin:0;padding:8px 10px;font-size:11.5px;line-height:1.5;color:var(--text-secondary);background:var(--bg-base);border:1px solid var(--border-subtle);border-radius:5px;overflow-x:auto;white-space:pre;max-height:320px;overflow-y:auto}.detail-rel-label[_ngcontent-%COMP%]{font-size:10.5px;margin-bottom:3px}.detail-rel-item[_ngcontent-%COMP%]{font-size:11.5px;color:var(--text-secondary);padding:1px 0}.thinking[_ngcontent-%COMP%]{padding-left:30px;font-size:12px}.thinking-dot[_ngcontent-%COMP%]{width:6px;height:6px;border-radius:50%;background:var(--accent);animation:skeleton 1s infinite;flex-shrink:0}.composer[_ngcontent-%COMP%]{padding:12px 24px 18px;border-top:1px solid var(--border-subtle);flex-shrink:0}.composer-inner[_ngcontent-%COMP%]{max-width:760px;margin:0 auto;position:relative}.composer-box[_ngcontent-%COMP%]{align-items:flex-end;background:var(--bg-panel);border:1px solid var(--border-subtle);border-radius:12px;padding:8px;gap:8px}.attach-btn[_ngcontent-%COMP%]{padding:7px;flex-shrink:0}.send-btn[_ngcontent-%COMP%]{padding:7px 10px;flex-shrink:0}.composer-ta[_ngcontent-%COMP%]{flex:1;resize:none;background:transparent;border:none;outline:none;color:var(--text-primary);font-size:13.5px;font-family:var(--font-ui);line-height:1.5;padding:5px 2px;max-height:120px}.composer-ta[_ngcontent-%COMP%]::placeholder{color:var(--text-muted)}.composer-footer[_ngcontent-%COMP%]{margin-top:8px;align-items:center}.slash-menu[_ngcontent-%COMP%]{position:absolute;bottom:100%;left:0;margin-bottom:8px;background:var(--bg-elevated);border:1px solid var(--border-strong);border-radius:8px;box-shadow:var(--shadow-pop);padding:5px;width:320px;z-index:10}.slash-item[_ngcontent-%COMP%]{width:100%;background:transparent;border:none;cursor:pointer;padding:6px 9px;border-radius:5px;text-align:left;color:inherit;font-family:var(--font-ui)}.slash-item[_ngcontent-%COMP%]:hover{background:var(--bg-hover)}.slash-item[_ngcontent-%COMP%]:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.ctx-collapsed[_ngcontent-%COMP%]{width:40px;flex-shrink:0;border-left:1px solid var(--border-subtle);display:flex;justify-content:center;padding-top:12px}.ctx-panel[_ngcontent-%COMP%]{width:264px;flex-shrink:0;border-left:1px solid var(--border-subtle);background:var(--bg-panel);display:flex;flex-direction:column;min-height:0}.ctx-header[_ngcontent-%COMP%]{padding:11px 13px;border-bottom:1px solid var(--border-subtle);align-items:center}.ctx-body[_ngcontent-%COMP%]{flex:1;padding:13px;overflow-y:auto;min-height:0}.ctx-row[_ngcontent-%COMP%]{padding:5px 0;align-items:flex-start}.ctx-footer[_ngcontent-%COMP%]{padding:12px;border-top:1px solid var(--border-subtle)}.toggle-btn[_ngcontent-%COMP%]{width:30px;height:17px;border-radius:999px;border:none;cursor:pointer;padding:2px;background:var(--bg-elevated);display:flex;justify-content:flex-start;align-items:center;transition:background .12s,justify-content .12s;flex-shrink:0}.toggle-btn.on[_ngcontent-%COMP%]{background:var(--accent);justify-content:flex-end}.toggle-btn[_ngcontent-%COMP%]:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.toggle-thumb[_ngcontent-%COMP%]{width:13px;height:13px;border-radius:50%;background:#fff;flex-shrink:0}.eyebrow[_ngcontent-%COMP%]{color:var(--text-muted);font-size:10.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}.muted[_ngcontent-%COMP%]{color:var(--text-muted)}.mono[_ngcontent-%COMP%]{font-family:var(--font-mono)}.pill[_ngcontent-%COMP%]{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;padding:3px 9px;border-radius:999px;line-height:1.4;white-space:nowrap}.grow[_ngcontent-%COMP%]{flex:1}"],changeDetection:0})};export{Y as Chat};
@@ -142,5 +142,5 @@
142
142
  })();
143
143
  </script>
144
144
  <app-root></app-root>
145
- <link rel="modulepreload" href="chunk-2IVSOVQZ.js"><link rel="modulepreload" href="chunk-YV5LB22R.js"><link rel="modulepreload" href="chunk-DOKZOFKM.js"><link rel="modulepreload" href="chunk-PAO2624Y.js"><link rel="modulepreload" href="chunk-PBRCKHEQ.js"><link rel="modulepreload" href="chunk-3H2EZLZN.js"><link rel="modulepreload" href="chunk-SYGIPC2P.js"><link rel="modulepreload" href="chunk-Q7L6LLAK.js"><script src="main-FHGJXJSF.js" type="module"></script></body>
145
+ <link rel="modulepreload" href="chunk-2IVSOVQZ.js"><link rel="modulepreload" href="chunk-YV5LB22R.js"><link rel="modulepreload" href="chunk-DOKZOFKM.js"><link rel="modulepreload" href="chunk-PAO2624Y.js"><link rel="modulepreload" href="chunk-PBRCKHEQ.js"><link rel="modulepreload" href="chunk-3H2EZLZN.js"><link rel="modulepreload" href="chunk-SYGIPC2P.js"><link rel="modulepreload" href="chunk-Q7L6LLAK.js"><script src="main-A6QZ3DLK.js" type="module"></script></body>
146
146
  </html>
@@ -1 +1 @@
1
- import{a as q,b as fe}from"./chunk-2IVSOVQZ.js";import{a as ge}from"./chunk-YV5LB22R.js";import{b as le,c as D,d as pe,e as de,f as ce}from"./chunk-DOKZOFKM.js";import{a as me,b as ue,c as z}from"./chunk-PAO2624Y.js";import{a as I}from"./chunk-PBRCKHEQ.js";import{a as V}from"./chunk-3H2EZLZN.js";import{$ as ie,Aa as p,Ba as c,Ca as v,Da as M,E as B,Ga as P,Ha as $,I as g,Ia as b,Ja as L,Ka as d,M as C,N as y,R as te,Ua as k,V as ne,Va as s,W as w,Wa as h,Xa as A,Ya as ae,ca as oe,cb as re,da as Z,ea as o,ka as x,lb as T,ob as N,pb as H,qa as j,ra as m,sa as u,va as S,wa as O,wb as se,xa as f,ya as a,za as r,zb as R}from"./chunk-SYGIPC2P.js";import"./chunk-Q7L6LLAK.js";var ve=[{path:"",pathMatch:"full",redirectTo:"dashboard"},{path:"dashboard",loadComponent:()=>import("./chunk-7ULUC3DC.js").then(e=>e.Dashboard),data:{nav:"dashboard",title:"Dashboard"}},{path:"graph",loadComponent:()=>import("./chunk-YVAUYHOB.js").then(e=>e.Graph),data:{nav:"graph",title:"Graph"}},{path:"specs",loadComponent:()=>import("./chunk-QJSQ3ORB.js").then(e=>e.Specs),data:{nav:"specs",title:"Specs"}},{path:"specs/:id",loadComponent:()=>import("./chunk-U6CWVOZO.js").then(e=>e.SpecDetail),data:{nav:"specs",title:"Spec"}},{path:"drift",loadComponent:()=>import("./chunk-H2TR6BKW.js").then(e=>e.Drift),data:{nav:"drift",title:"Drift queue"}},{path:"improvements",loadComponent:()=>import("./chunk-BDF4LLKR.js").then(e=>e.Improvements),data:{nav:"improvements",title:"Improvements"}},{path:"maintainability",loadComponent:()=>import("./chunk-FYXVIWM2.js").then(e=>e.Maintainability),data:{nav:"maintainability",title:"Maintainability"}},{path:"domain",loadComponent:()=>import("./chunk-NY5CBIVU.js").then(e=>e.Domain),data:{nav:"domain",title:"Domain"}},{path:"workflows",loadComponent:()=>import("./chunk-KXMWAQNB.js").then(e=>e.Workflows),data:{nav:"workflows",title:"Workflows"}},{path:"runs",loadComponent:()=>import("./chunk-IXQIWN6Z.js").then(e=>e.Runs),data:{nav:"runs",title:"Runs"}},{path:"runs/:id",loadComponent:()=>import("./chunk-HZYJAXHL.js").then(e=>e.RunDetail),data:{nav:"runs",title:"Run detail"}},{path:"chat",loadComponent:()=>import("./chunk-VPUORMLB.js").then(e=>e.Chat),data:{nav:"chat",title:"Chat"}},{path:"sessions",loadComponent:()=>import("./chunk-7ALHQYYG.js").then(e=>e.Sessions),data:{nav:"sessions",title:"Sessions"}},{path:"sessions/:id",loadComponent:()=>import("./chunk-C6I5R6UZ.js").then(e=>e.SessionDetail),data:{nav:"sessions",title:"Session detail"}},{path:"heatmap",loadComponent:()=>import("./chunk-EAWW6PQJ.js").then(e=>e.Heatmap),data:{nav:"heatmap",title:"Heatmap"}},{path:"costs",loadComponent:()=>import("./chunk-G2FTQVMJ.js").then(e=>e.Costs),data:{nav:"costs",title:"Costs"}},{path:"specship-impact",loadComponent:()=>import("./chunk-5CR7TMVN.js").then(e=>e.SpecshipImpact),data:{nav:"specship-impact",title:"SpecShip Impact"}},{path:"compare",loadComponent:()=>import("./chunk-7V4MGZCG.js").then(e=>e.Compare),data:{nav:"compare",title:"Compare projects"}},{path:"tips",loadComponent:()=>import("./chunk-D7HQMX7C.js").then(e=>e.Tips),data:{nav:"tips",title:"Tips"}},{path:"memory",loadComponent:()=>import("./chunk-YPWBUJH7.js").then(e=>e.Memory),data:{nav:"memory",title:"Memory"}},{path:"design",loadComponent:()=>import("./chunk-4G7XU55L.js").then(e=>e.Design),data:{nav:"design",title:"Design system"}},{path:"mcp",loadComponent:()=>import("./chunk-BZ5SMVXG.js").then(e=>e.Mcp),data:{nav:"mcp",title:"MCP servers"}},{path:"mcp/:id",loadComponent:()=>import("./chunk-Z23KACD6.js").then(e=>e.McpDetail),data:{nav:"mcp",title:"MCP server"}},{path:"settings",loadComponent:()=>import("./chunk-G2MHCJUR.js").then(e=>e.Settings),data:{nav:"settings",title:"Settings"}},{path:"**",redirectTo:"dashboard"}];var he={providers:[ne(),ce(ve)]};var _e=e=>["/",e],be=(e,n)=>n.group,xe=(e,n)=>n.id;function Ce(e,n){e&1&&(a(0,"div",3),s(1,"SpecShip "),a(2,"span",14),s(3,"Desktop"),r()())}function ye(e,n){if(e&1&&(a(0,"div",15),s(1),r()),e&2){let t=d().$implicit;o(),h(t.group)}}function Pe(e,n){if(e&1&&(a(0,"span",18),s(1),r()),e&2){let t=d(2).$implicit,i=d(2);j("data-kind",t.badgeKind),o(),h(i.badgeOf(t))}}function ke(e,n){if(e&1&&(a(0,"span",11),s(1),r(),m(2,Pe,2,2,"span",18)),e&2){let t=d().$implicit,i=d(2);o(),h(t.label),o(),u(i.badgeOf(t)>0?2:-1)}}function we(e,n){if(e&1&&(a(0,"a",16),p(1,"span",8),a(2,"span",9),p(3,"app-icon",17),r(),m(4,ke,3,2),r()),e&2){let t=n.$implicit,i=d(2);f("routerLink",re(5,_e,t.id))("title",i.collapsed()?t.label:null),o(3),f("name",t.icon)("size",16),o(),u(i.collapsed()?-1:4)}}function Me(e,n){if(e&1&&(a(0,"div",5),m(1,ye,2,1,"div",15),S(2,we,5,7,"a",16,xe),r()),e&2){let t=n.$implicit,i=d();o(),u(i.collapsed()?-1:1),o(),O(t.items)}}function Se(e,n){e&1&&(a(0,"span",11),s(1,"Design system"),r())}function Oe(e,n){e&1&&(a(0,"span",11),s(1,"Settings"),r())}var G=class e{collapsed=H(!1);api=g(V);projects=g(I);status=z(this.api,()=>`/api/status${this.projects.projectQuery()}`);tips=z(this.api,()=>"/api/claude/tips");improvements=z(this.api,()=>"/api/reflect?state=open");nav=[{group:"Project",items:[{id:"dashboard",label:"Dashboard",icon:"dashboard"},{id:"graph",label:"Graph",icon:"graph"},{id:"specs",label:"Specs",icon:"book"},{id:"domain",label:"Domain",icon:"layers"},{id:"drift",label:"Drift queue",icon:"drift",badge:()=>this.status.state().data?.drift??0,badgeKind:"warn"},{id:"maintainability",label:"Maintainability",icon:"wrench"},{id:"improvements",label:"Improvements",icon:"sparkles",badge:()=>(this.improvements.state().data?.proposals??[]).filter(n=>n.severity==="high").length,badgeKind:"error"},{id:"workflows",label:"Workflows",icon:"workflow"},{id:"runs",label:"Runs",icon:"play"},{id:"chat",label:"Chat",icon:"chat"}]},{group:"Claude Code",items:[{id:"sessions",label:"Sessions",icon:"sessions"},{id:"heatmap",label:"Heatmap",icon:"heatmap"},{id:"costs",label:"Costs",icon:"dollar"},{id:"specship-impact",label:"SpecShip Impact",icon:"graph"},{id:"compare",label:"Compare projects",icon:"compare"},{id:"memory",label:"Memory",icon:"memory"},{id:"mcp",label:"MCP",icon:"plug"},{id:"tips",label:"Tips",icon:"tips",badge:()=>(this.tips.state().data?.tips??[]).filter(n=>n.severity!=="info").length,badgeKind:"error"}]}];badgeOf(n){return n.badge?n.badge():0}static \u0275fac=function(t){return new(t||e)};static \u0275cmp=x({type:e,selectors:[["app-sidebar"]],inputs:{collapsed:[1,"collapsed"]},decls:18,vars:10,consts:[[1,"sidebar"],[1,"brand"],[3,"size"],[1,"brand-text"],[1,"scroll-y","nav-scroll"],[1,"nav-group"],[1,"pinned"],["routerLink","/design","routerLinkActive","active",1,"nav-item",3,"title"],[1,"indicator"],[1,"nav-icon"],["name","palette",3,"size"],[1,"nav-label"],["routerLink","/settings","routerLinkActive","active",1,"nav-item",3,"title"],["name","settings",3,"size"],[1,"muted"],[1,"eyebrow","group-label"],["routerLinkActive","active",1,"nav-item",3,"routerLink","title"],[3,"name","size"],[1,"badge"]],template:function(t,i){t&1&&(a(0,"nav",0)(1,"div",1),p(2,"app-logo-mark",2),m(3,Ce,4,0,"div",3),r(),a(4,"div",4),S(5,Me,4,1,"div",5,be),r(),a(7,"div",6)(8,"a",7),p(9,"span",8),a(10,"span",9),p(11,"app-icon",10),r(),m(12,Se,2,0,"span",11),r(),a(13,"a",12),p(14,"span",8),a(15,"span",9),p(16,"app-icon",13),r(),m(17,Oe,2,0,"span",11),r()()()),t&2&&(k("collapsed",i.collapsed()),o(2),f("size",26),o(),u(i.collapsed()?-1:3),o(2),O(i.nav),o(3),f("title",i.collapsed()?"Design system":null),o(3),f("size",16),o(),u(i.collapsed()?-1:12),o(),f("title",i.collapsed()?"Settings":null),o(3),f("size",16),o(),u(i.collapsed()?-1:17))},dependencies:[pe,de,R,ge],styles:['@charset "UTF-8";[_nghost-%COMP%]{display:contents}.sidebar[_ngcontent-%COMP%]{width:var(--sidebar-w);flex-shrink:0;background:var(--bg-panel);border-right:1px solid var(--border-subtle);display:flex;flex-direction:column;transition:width .12s;overflow:hidden;font-family:var(--font-ui)}.sidebar.collapsed[_ngcontent-%COMP%]{width:var(--sidebar-w-collapsed)}.brand[_ngcontent-%COMP%]{display:flex;align-items:center;gap:8px;padding:15px 16px}.collapsed[_ngcontent-%COMP%] .brand[_ngcontent-%COMP%]{justify-content:center;padding:14px 0}.brand-text[_ngcontent-%COMP%]{font-weight:600;font-size:13.5px;letter-spacing:-.01em}.brand-text[_ngcontent-%COMP%] .muted[_ngcontent-%COMP%]{color:var(--text-muted);font-weight:400}.nav-scroll[_ngcontent-%COMP%]{flex:1;padding:4px 8px}.nav-group[_ngcontent-%COMP%]{margin-bottom:10px}.group-label[_ngcontent-%COMP%]{padding:8px 8px 4px}.nav-item[_ngcontent-%COMP%]{position:relative;display:flex;align-items:center;gap:10px;height:32px;padding:0 10px;border-radius:7px;text-decoration:none;margin-bottom:1px;color:var(--text-secondary);font-weight:450;font-size:13px;transition:background .1s}.collapsed[_ngcontent-%COMP%] .nav-item[_ngcontent-%COMP%]{padding:0;justify-content:center}.nav-item[_ngcontent-%COMP%]:hover{background:var(--bg-hover);color:var(--text-primary)}.nav-item.active[_ngcontent-%COMP%]{background:var(--bg-active);color:var(--text-primary);font-weight:600}.nav-item.active[_ngcontent-%COMP%] .indicator[_ngcontent-%COMP%]{position:absolute;left:0;top:7px;bottom:7px;width:2.5px;border-radius:2px;background:var(--accent)}.nav-icon[_ngcontent-%COMP%]{width:16px;height:16px;flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;color:var(--text-secondary)}.nav-item[_ngcontent-%COMP%]:hover .nav-icon[_ngcontent-%COMP%]{color:var(--text-primary)}.nav-item.active[_ngcontent-%COMP%] .nav-icon[_ngcontent-%COMP%]{color:var(--accent)}.nav-label[_ngcontent-%COMP%]{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.badge[_ngcontent-%COMP%]{font-family:var(--font-mono);font-variant-numeric:tabular-nums;font-size:10px;font-weight:700;padding:1px 6px;min-width:18px;border-radius:999px;display:inline-flex;justify-content:center;color:var(--text-secondary);background:#ffffff0f}.badge[data-kind=error][_ngcontent-%COMP%]{color:var(--error);background:var(--error-soft)}.badge[data-kind=warn][_ngcontent-%COMP%]{color:var(--warn);background:var(--warn-soft)}.pinned[_ngcontent-%COMP%]{padding:8px;border-top:1px solid var(--border-subtle)}'],changeDetection:0})};function Ee(e,n){if(e&1&&(c(0,"span",8),s(1),v(),M(2,"span",1),c(3,"span",9)(4,"span",10),s(5,"nodes"),v(),c(6,"span",4),s(7),v()(),c(8,"span",9)(9,"span",10),s(10,"edges"),v(),c(11,"span",4),s(12),v()(),M(13,"span",1),c(14,"span",9)(15,"span",10),s(16,"drift"),v(),c(17,"span",4),s(18),v()(),M(19,"span",11),c(20,"span",9)(21,"span",10),s(22,"indexed"),v(),c(23,"span",4),s(24),v()()),e&2){let t=d(),i=t.status.state().data;o(),h(i.projectPath),o(6),h(i.nodeCount.toLocaleString()),o(5),h(i.edgeCount.toLocaleString()),o(5),k("warn",i.drift>0)("success",i.drift===0),o(),h(i.drift),o(6),h(t.indexedLabel())}}function je(e,n){e&1&&(c(0,"span",12),s(1,"loading status\u2026"),v(),M(2,"span",11))}function Te(e,n){if(e&1&&(c(0,"span",12),s(1),v(),M(2,"span",11)),e&2){let t=d();o(),h(t.conn.online()?"no backend":"offline \u2014 no cached data")}}function De(e,n){e&1&&(c(0,"span",5),s(1),v()),e&2&&(o(),A("\xB7 ",n))}function Ie(e,n){e&1&&(c(0,"span",6),s(1,"\u26A0"),v()),e&2&&$("title",n)}var K=class e{api=g(V);projects=g(I);refreshSvc=g(ue);conn=g(me);status=z(this.api,()=>`/api/status${this.projects.projectQuery()}`);offlineAge=T(()=>{let n=this.status.state().cachedAt??this.conn.lastOnlineAt();if(n===null)return null;let t=Date.now()-n;return t<1e4?"just now":t<6e4?`${Math.round(t/1e3)}s ago`:t<36e5?`${Math.round(t/6e4)}m ago`:t<864e5?`${Math.round(t/36e5)}h ago`:`${Math.round(t/864e5)}d ago`});indexedLabel=T(()=>{let n=this.status.state().data?.lastIndexed;if(!n)return"never";try{let t=new Date(n),i=Date.now()-t.getTime();return i<6e4?"just now":i<36e5?`${Math.round(i/6e4)}m ago`:i<864e5?`${Math.round(i/36e5)}h ago`:t.toLocaleDateString()}catch{return String(n)}});async refresh(){await this.refreshSvc.triggerGlobalRefresh()}static \u0275fac=function(t){return new(t||e)};static \u0275cmp=x({type:e,selectors:[["app-status-strip"]],decls:13,vars:14,consts:[[1,"status-strip"],[1,"divider"],[1,"seg","conn",3,"title"],[1,"conn-dot"],[1,"mono","val"],[1,"muted","conn-age"],[1,"refresh-error","mono",3,"title"],[1,"refresh",3,"click","disabled","title"],[1,"seg","mono"],[1,"seg"],[1,"lbl"],[1,"grow"],[1,"mono","muted"]],template:function(t,i){if(t&1&&(c(0,"div",0),m(1,Ee,25,9)(2,je,3,0)(3,Te,3,1),M(4,"span",1),c(5,"span",2),M(6,"span",3),c(7,"span",4),s(8),v(),m(9,De,2,1,"span",5),v(),m(10,Ie,2,1,"span",6),c(11,"button",7),L("click",function(){return i.refresh()}),s(12,"\u21BB"),v()()),t&2){let l,_;o(),u(i.status.state().data?1:i.status.state().loading?2:3),o(4),$("title",i.conn.online()?"Connected to the SpecShip server":"Server unreachable \u2014 showing the last cached data"),o(),k("live",i.conn.online())("off",!i.conn.online()),o(2),h(i.conn.online()?"Live":"Offline"),o(),u((l=!i.conn.online()&&i.offlineAge())?9:-1,l),o(),u((_=i.refreshSvc.error())?10:-1,_),o(),k("spinning",i.refreshSvc.loading()),$("disabled",i.refreshSvc.loading()||!i.conn.online())("title",i.conn.online()?i.refreshSvc.loading()?"Refreshing\u2026":"Refresh: sync index + re-ingest Claude Code transcripts. Updates Sessions, Heatmap, Costs, Memory, Drift, Graph.":"Offline \u2014 reconnect to refresh"),j("aria-label",i.conn.online()?i.refreshSvc.loading()?"Refreshing\u2026":"Refresh everything \u2014 sync index + ingest Claude Code transcripts":"Offline \u2014 reconnect to refresh")}},styles:["[_nghost-%COMP%]{display:contents}.status-strip[_ngcontent-%COMP%]{height:var(--status-h);flex-shrink:0;display:flex;align-items:center;gap:14px;padding:0 14px;border-bottom:1px solid var(--border-subtle);background:var(--bg-panel-2);font-size:11.5px;overflow:hidden;white-space:nowrap}.seg[_ngcontent-%COMP%]{display:flex;align-items:center;gap:6px;flex-shrink:0}.lbl[_ngcontent-%COMP%]{color:var(--text-muted);font-size:11px}.val[_ngcontent-%COMP%]{color:var(--text-secondary);font-variant-numeric:tabular-nums}.val.warn[_ngcontent-%COMP%]{color:var(--warn)}.val.success[_ngcontent-%COMP%]{color:var(--success)}.divider[_ngcontent-%COMP%]{width:1px;height:12px;background:var(--border-subtle);flex-shrink:0}.grow[_ngcontent-%COMP%]{flex:1}.muted[_ngcontent-%COMP%]{color:var(--text-muted)}.refresh[_ngcontent-%COMP%]{background:transparent;border:none;color:var(--text-muted);cursor:pointer;font-size:14px;padding:2px 6px;border-radius:4px;transition:background .12s,color .12s}.refresh[_ngcontent-%COMP%]:hover:not(:disabled){background:var(--bg-hover);color:var(--text-primary)}.refresh[_ngcontent-%COMP%]:disabled{cursor:progress;opacity:.8}.refresh[_ngcontent-%COMP%]:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.refresh.spinning[_ngcontent-%COMP%]{display:inline-block;animation:_ngcontent-%COMP%_spin .9s linear infinite;color:var(--accent)}@keyframes _ngcontent-%COMP%_spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.refresh-error[_ngcontent-%COMP%]{color:var(--warn);font-size:12px;padding:0 2px;cursor:help}.conn[_ngcontent-%COMP%]{gap:5px}.conn-dot[_ngcontent-%COMP%]{width:6px;height:6px;border-radius:50%;flex-shrink:0}.conn-dot.live[_ngcontent-%COMP%]{background:var(--success);box-shadow:0 0 6px var(--success)}.conn-dot.off[_ngcontent-%COMP%]{background:var(--warn);box-shadow:0 0 6px var(--warn)}.conn-age[_ngcontent-%COMP%]{font-size:11px}"],changeDetection:0})};var ze=(e,n)=>n.slug;function Re(e,n){e&1&&p(0,"span",8)}function Le(e,n){e&1&&p(0,"span",9)}function Ae(e,n){e&1&&p(0,"span",10)}function Ne(e,n){if(e&1&&(a(0,"span",7),s(1),r(),m(2,Re,1,0,"span",8)(3,Le,1,0,"span",9)(4,Ae,1,0,"span",10)),e&2){let t=n,i=d();o(),h(i.shortPath(t.path)),o(),u(t.exists?t.initialized?4:3:2)}}function Fe(e,n){e&1&&(a(0,"span",3),s(1,"Select project"),r())}function Be(e,n){if(e&1){let t=P();a(0,"button",20),b("click",function(){C(t);let l=d(2);return y(l.clear())}),p(1,"app-icon",21),s(2," Clear "),r()}e&2&&(o(),f("size",11))}function $e(e,n){e&1&&(a(0,"span",16),s(1,"\xB7 loading\u2026"),r())}function He(e,n){if(e&1&&(a(0,"span",16),s(1),r()),e&2){let t=d(2);o(),A("\xB7 ",t.filtered().length," shown \xB7 live")}}function Ve(e,n){e&1&&(a(0,"div",18),s(1,"No projects match."),r())}function qe(e,n){e&1&&(a(0,"span",25),s(1,"missing"),r())}function Ge(e,n){e&1&&(a(0,"span",26),s(1,"not init"),r())}function Ke(e,n){e&1&&(a(0,"span",27),s(1,"initialized"),r())}function We(e,n){if(e&1&&(a(0,"span"),s(1),r()),e&2){let t=d().$implicit,i=d(2);o(),A("\xB7 ",i.formatRelative(t.lastModifiedMs))}}function Qe(e,n){e&1&&p(0,"app-icon",29),e&2&&f("size",13)}function Je(e,n){if(e&1){let t=P();a(0,"button",22),b("click",function(){let l=C(t).$implicit,_=d(2);return y(_.pick(l.slug))}),a(1,"div",23)(2,"div",24)(3,"span",7),s(4),r(),m(5,qe,2,0,"span",25)(6,Ge,2,0,"span",26)(7,Ke,2,0,"span",27),r(),a(8,"div",28)(9,"span"),s(10),r(),m(11,We,2,1,"span"),r()(),m(12,Qe,1,1,"app-icon",29),r()}if(e&2){let t=n.$implicit,i=d(2);k("active",i.projects.activeSlug()===t.slug),o(4),h(i.shortPath(t.path)),o(),u(t.exists?t.initialized?7:6:5),o(5),ae("",t.sessionCount," session",t.sessionCount===1?"":"s"),o(),u(t.lastModifiedMs>0?11:-1),o(),u(i.projects.activeSlug()===t.slug?12:-1)}}function Ue(e,n){if(e&1){let t=P();a(0,"div",6)(1,"div",11),p(2,"app-icon",12),a(3,"input",13),b("input",function(l){C(t);let _=d();return y(_.onQuery(l.target.value))}),r(),m(4,Be,3,1,"button",14),r(),a(5,"div",15)(6,"span"),s(7),r(),m(8,$e,2,0,"span",16)(9,He,2,1,"span",16),r(),a(10,"div",17),m(11,Ve,2,0,"div",18),S(12,Je,13,8,"button",19,ze),r()()}if(e&2){let t=d();o(2),f("size",13),o(),f("value",t.query()),o(),u(t.projects.activeSlug()?4:-1),o(3),h(t.projects.claudeRoot()??"~/.claude/projects"),o(),u(t.projects.loading()?8:9),o(3),u(t.filtered().length===0&&!t.projects.loading()?11:-1),o(),O(t.filtered())}}var W=class e{host=g(ie);projects=g(I);open=w(!1);query=w("");filtered=T(()=>{let n=this.query().trim().toLowerCase(),t=this.projects.projects();return n?t.filter(i=>i.path.toLowerCase().includes(n)||i.slug.toLowerCase().includes(n)):t});badge=T(()=>this.projects.projects().length);toggle(){this.open.update(n=>!n)}pick(n){this.projects.setActive(n),this.open.set(!1),this.query.set("")}clear(){this.projects.setActive(null),this.open.set(!1)}onQuery(n){this.query.set(n)}onDocumentClick(n){if(!this.open())return;let t=n.target;t&&!this.host.nativeElement.contains(t)&&this.open.set(!1)}onEscape(){this.open()&&this.open.set(!1)}formatRelative(n){if(!n)return"";let t=Date.now()-n,i=Math.round(t/1e3);if(i<60)return`${i}s ago`;let l=Math.round(i/60);if(l<60)return`${l}m ago`;let _=Math.round(l/60);if(_<24)return`${_}h ago`;let ee=Math.round(_/24);return ee<30?`${ee}d ago`:new Date(n).toLocaleDateString()}shortPath(n){if(!n)return"";let t=this.detectHome();return t&&n===t?"~":t&&n.startsWith(t+"/")?"~/"+n.slice(t.length+1):n}detectHome(){let n=this.projects.claudeRoot();if(!n)return null;let t="/.claude/projects";return n.endsWith(t)?n.slice(0,-t.length):null}static \u0275fac=function(t){return new(t||e)};static \u0275cmp=x({type:e,selectors:[["app-project-picker"]],hostBindings:function(t,i){t&1&&b("click",function(_){return i.onDocumentClick(_)},Z)("keydown.escape",function(){return i.onEscape()},Z)},decls:9,vars:9,consts:[[1,"wrap"],["type","button","aria-label","Switch project",1,"trigger",3,"click"],["name","folder",3,"size"],[1,"placeholder"],[1,"badge","mono"],[3,"name","size"],["role","dialog","aria-label","Pick a project",1,"popover"],[1,"path","mono"],["title","path no longer exists on disk",1,"status-dot","missing"],["title","not yet specship init'd",1,"status-dot","warn"],["title","initialized",1,"status-dot","ok"],[1,"popover-head","row","gap-8"],["name","search",3,"size"],["type","text","placeholder","Filter projects\u2026","autofocus","",1,"popover-input",3,"input","value"],["title","Clear selection",1,"btn","btn-ghost","btn-xs"],[1,"popover-meta","mono"],[1,"muted"],[1,"popover-list","scroll-y"],[1,"empty"],["type","button",1,"row-item",3,"active"],["title","Clear selection",1,"btn","btn-ghost","btn-xs",3,"click"],["name","x",3,"size"],["type","button",1,"row-item",3,"click"],[1,"col","gap-2","grow"],[1,"row","gap-6","path-line"],[1,"pill","missing"],[1,"pill","warn"],[1,"pill","ok"],[1,"meta","row","gap-8","mono","muted"],["name","check",3,"size"]],template:function(t,i){if(t&1&&(a(0,"div",0)(1,"button",1),b("click",function(){return i.toggle()}),p(2,"app-icon",2),m(3,Ne,5,2)(4,Fe,2,0,"span",3),a(5,"span",4),s(6),r(),p(7,"app-icon",5),r(),m(8,Ue,14,6,"div",6),r()),t&2){let l;k("open",i.open()),o(),j("aria-expanded",i.open()),o(),f("size",13),o(),u((l=i.projects.active())?3:4,l),o(3),h(i.badge()),o(),f("name",i.open()?"chevronDown":"chevronRight")("size",11),o(),u(i.open()?8:-1)}},dependencies:[R],styles:["[_nghost-%COMP%]{display:inline-flex;min-width:0}.wrap[_ngcontent-%COMP%]{position:relative;min-width:0}.trigger[_ngcontent-%COMP%]{height:30px;padding:0 11px 0 10px;background:var(--bg-panel);border:1px solid var(--border-subtle);border-radius:8px;color:var(--text-secondary);display:inline-flex;align-items:center;gap:8px;font-family:var(--font-ui);font-size:12.5px;cursor:pointer;max-width:360px;min-width:0;transition:background .1s,border-color .1s}.trigger[_ngcontent-%COMP%]:hover, .wrap.open[_ngcontent-%COMP%] .trigger[_ngcontent-%COMP%]{background:var(--bg-panel-2);border-color:var(--border-strong);color:var(--text-primary)}.trigger[_ngcontent-%COMP%] app-icon[_ngcontent-%COMP%]{color:var(--text-muted);flex-shrink:0}.trigger[_ngcontent-%COMP%]:hover app-icon[_ngcontent-%COMP%]{color:var(--text-secondary)}.path[_ngcontent-%COMP%]{font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.placeholder[_ngcontent-%COMP%]{color:var(--text-muted);font-style:italic}.status-dot[_ngcontent-%COMP%]{width:6px;height:6px;border-radius:50%;flex-shrink:0}.status-dot.ok[_ngcontent-%COMP%]{background:var(--success)}.status-dot.warn[_ngcontent-%COMP%]{background:var(--warn)}.status-dot.missing[_ngcontent-%COMP%]{background:var(--error)}.badge[_ngcontent-%COMP%]{font-size:10px;padding:1px 6px;border-radius:999px;background:var(--bg-active);color:var(--text-muted);font-variant-numeric:tabular-nums;flex-shrink:0}.popover[_ngcontent-%COMP%]{position:absolute;top:calc(100% + 6px);left:0;z-index:30;width:420px;max-width:calc(100vw - 40px);background:var(--bg-elevated);border:1px solid var(--border-strong);border-radius:10px;box-shadow:var(--shadow-pop);display:flex;flex-direction:column;max-height:480px;animation:_ngcontent-%COMP%_pp-in .12s ease-out}@keyframes _ngcontent-%COMP%_pp-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.popover-head[_ngcontent-%COMP%]{padding:10px 12px 8px;border-bottom:1px solid var(--border-subtle)}.popover-head[_ngcontent-%COMP%] app-icon[_ngcontent-%COMP%]{color:var(--text-muted);flex-shrink:0}.popover-input[_ngcontent-%COMP%]{flex:1;min-width:0;background:transparent;border:none;outline:none;font-family:var(--font-ui);font-size:13px;color:var(--text-primary)}.popover-input[_ngcontent-%COMP%]::placeholder{color:var(--text-muted)}.popover-meta[_ngcontent-%COMP%]{padding:6px 12px 8px;font-size:10.5px;color:var(--text-secondary);border-bottom:1px solid var(--border-subtle);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.popover-meta[_ngcontent-%COMP%] .muted[_ngcontent-%COMP%]{color:var(--text-muted)}.popover-list[_ngcontent-%COMP%]{flex:1;min-height:0;padding:4px}.row-item[_ngcontent-%COMP%]{width:100%;display:flex;align-items:center;gap:10px;padding:9px 10px;border:none;border-radius:7px;background:transparent;color:inherit;cursor:pointer;font-family:inherit;text-align:left;transition:background 80ms}.row-item[_ngcontent-%COMP%]:hover{background:var(--bg-hover)}.row-item.active[_ngcontent-%COMP%]{background:var(--accent-soft);color:var(--text-primary)}.row-item.active[_ngcontent-%COMP%] app-icon[_ngcontent-%COMP%]{color:var(--accent)}.path-line[_ngcontent-%COMP%]{min-width:0}.path-line[_ngcontent-%COMP%] .path[_ngcontent-%COMP%]{font-size:12.5px;font-weight:500;color:var(--text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.meta[_ngcontent-%COMP%]{font-size:10.5px}.pill[_ngcontent-%COMP%]{font-size:10px;font-weight:600;padding:1px 7px;border-radius:999px;white-space:nowrap;flex-shrink:0}.pill.ok[_ngcontent-%COMP%]{color:var(--success);background:var(--success-soft)}.pill.warn[_ngcontent-%COMP%]{color:var(--warn);background:var(--warn-soft)}.pill.missing[_ngcontent-%COMP%]{color:var(--error);background:var(--error-soft)}.empty[_ngcontent-%COMP%]{text-align:center;color:var(--text-muted);font-size:12px;padding:18px 12px}.muted[_ngcontent-%COMP%]{color:var(--text-muted)}.mono[_ngcontent-%COMP%]{font-family:var(--font-mono)}.grow[_ngcontent-%COMP%]{flex:1;min-width:0}.row[_ngcontent-%COMP%]{display:flex;align-items:center}.col[_ngcontent-%COMP%]{display:flex;flex-direction:column}.gap-2[_ngcontent-%COMP%]{gap:2px}.gap-6[_ngcontent-%COMP%]{gap:6px}.gap-8[_ngcontent-%COMP%]{gap:8px}.scroll-y[_ngcontent-%COMP%]{overflow-y:auto;overflow-x:hidden}"],changeDetection:0})};var Q=class e{deferred=null;canInstall=w(!1);constructor(){typeof window>"u"||window.matchMedia?.("(display-mode: standalone)").matches===!0||window.navigator.standalone===!0||(window.addEventListener("beforeinstallprompt",t=>{t.preventDefault(),this.deferred=t,this.canInstall.set(!0)}),window.addEventListener("appinstalled",()=>{this.deferred=null,this.canInstall.set(!1)}))}async install(){let n=this.deferred;if(n){this.deferred=null,this.canInstall.set(!1);try{await n.prompt(),await n.userChoice}catch{}}}static \u0275fac=function(t){return new(t||e)};static \u0275prov=B({token:e,factory:e.\u0275fac,providedIn:"root"})};function Ye(e,n){if(e&1){let t=P();a(0,"button",9),b("click",function(){C(t);let l=d();return y(l.onInstall())}),p(1,"app-icon",10),a(2,"span"),s(3,"Install"),r()()}e&2&&(o(),f("size",14))}var J=class e{toggleSidebar=N();openPalette=N();theme=g(q);effective=this.theme.effective;pwa=g(Q);onToggleTheme(){this.theme.toggle()}onMenuClick(){this.toggleSidebar.emit()}onPalette(){this.openPalette.emit()}onInstall(){this.pwa.install()}static \u0275fac=function(t){return new(t||e)};static \u0275cmp=x({type:e,selectors:[["app-top-bar"]],outputs:{toggleSidebar:"toggleSidebar",openPalette:"openPalette"},decls:14,vars:6,consts:[[1,"topbar"],["title","Toggle sidebar","aria-label","Toggle sidebar",1,"ghost","icon-btn",3,"click"],["name","menu",3,"size"],["aria-label","Open command palette",1,"palette-trigger",3,"click"],["name","search",3,"size"],[1,"grow"],["title","Install SpecShip as an app","aria-label","Install SpecShip as an app",1,"ghost",2,"display","inline-flex","align-items","center","gap","6px","padding","0 10px","height","30px","font-size","12px"],["aria-label","Toggle theme",1,"ghost","icon-btn",3,"click","title"],[3,"name","size"],["title","Install SpecShip as an app","aria-label","Install SpecShip as an app",1,"ghost",2,"display","inline-flex","align-items","center","gap","6px","padding","0 10px","height","30px","font-size","12px",3,"click"],["name","plus",3,"size"]],template:function(t,i){t&1&&(a(0,"div",0)(1,"button",1),b("click",function(){return i.onMenuClick()}),p(2,"app-icon",2),r(),p(3,"app-project-picker"),a(4,"button",3),b("click",function(){return i.onPalette()}),p(5,"app-icon",4),a(6,"span"),s(7,"Search or jump to\u2026"),r(),a(8,"kbd"),s(9,"\u2318K"),r()(),p(10,"span",5),m(11,Ye,4,1,"button",6),a(12,"button",7),b("click",function(){return i.onToggleTheme()}),p(13,"app-icon",8),r()()),t&2&&(o(2),f("size",16),o(3),f("size",13),o(6),u(i.pwa.canInstall()?11:-1),o(),f("title","Theme: "+i.effective()),o(),f("name",i.effective()==="dark"?"sun":"moon")("size",16))},dependencies:[R,W],styles:["[_nghost-%COMP%]{display:contents}.topbar[_ngcontent-%COMP%]{height:44px;flex-shrink:0;display:flex;align-items:center;gap:10px;padding:0 14px;border-bottom:1px solid var(--border-subtle);background:var(--bg-canvas)}.ghost[_ngcontent-%COMP%]{background:transparent;border:1px solid transparent;color:var(--text-secondary);cursor:pointer}.ghost[_ngcontent-%COMP%]:hover{background:var(--bg-hover);color:var(--text-primary)}.icon-btn[_ngcontent-%COMP%]{padding:6px 9px;border-radius:5px;font-size:16px}.palette-trigger[_ngcontent-%COMP%]{flex:1;max-width:380px;height:30px;padding:0 11px;background:var(--bg-panel);border:1px solid var(--border-subtle);border-radius:8px;cursor:text;color:var(--text-muted);font-size:12.5px;text-align:left;display:flex;align-items:center;gap:8px}.palette-trigger[_ngcontent-%COMP%]:hover{border-color:var(--border-strong);color:var(--text-secondary)}.palette-trigger[_ngcontent-%COMP%] app-icon[_ngcontent-%COMP%]{color:var(--text-muted)}.palette-trigger[_ngcontent-%COMP%] span[_ngcontent-%COMP%]{flex:1}kbd[_ngcontent-%COMP%]{font-family:var(--font-mono);font-size:10px;color:var(--text-muted);background:var(--bg-canvas);border:1px solid var(--border-subtle);border-radius:4px;padding:2px 6px}.grow[_ngcontent-%COMP%]{flex:1}"],changeDetection:0})};var Xe=(e,n)=>n.id;function Ze(e,n){if(e&1){let t=P();c(0,"button",6),L("click",function(){let l=C(t).$implicit,_=d(2);return y(_.go(l.id))}),c(1,"span",7),s(2),v(),c(3,"span",8),s(4),v()()}if(e&2){let t=n.$implicit;o(2),h(t.label),o(2),h(t.sub)}}function et(e,n){if(e&1){let t=P();c(0,"div",1),L("click",function(l){C(t);let _=d();return y(_.onBackdrop(l))}),c(1,"div",2)(2,"div",3)(3,"span"),s(4,"Jump to\u2026"),v(),c(5,"kbd"),s(6,"esc"),v()(),c(7,"div",4),S(8,Ze,5,2,"button",5,Xe),v()()()}if(e&2){let t=d();o(8),O(t.items)}}var tt=[{id:"dashboard",label:"Dashboard",sub:"/dashboard"},{id:"graph",label:"Graph",sub:"/graph"},{id:"specs",label:"Specs",sub:"/specs"},{id:"drift",label:"Drift queue",sub:"/drift"},{id:"workflows",label:"Workflows",sub:"/workflows"},{id:"runs",label:"Runs",sub:"/runs"},{id:"chat",label:"Chat",sub:"/chat"},{id:"sessions",label:"Sessions",sub:"/sessions"},{id:"heatmap",label:"Heatmap",sub:"/heatmap"},{id:"costs",label:"Costs",sub:"/costs"},{id:"compare",label:"Compare projects",sub:"/compare"},{id:"tips",label:"Tips",sub:"/tips"},{id:"settings",label:"Settings",sub:"/settings"}],U=class e{open=H(!1);close=N();router=g(D);items=tt;go(n){this.router.navigate(["/"+n]),this.close.emit()}onBackdrop(n){n.target===n.currentTarget&&this.close.emit()}static \u0275fac=function(t){return new(t||e)};static \u0275cmp=x({type:e,selectors:[["app-command-palette"]],inputs:{open:[1,"open"]},outputs:{close:"close"},decls:1,vars:1,consts:[[1,"backdrop"],[1,"backdrop",3,"click"],[1,"palette"],[1,"palette-header"],[1,"palette-list","scroll-y"],[1,"item"],[1,"item",3,"click"],[1,"label"],[1,"sub","mono"]],template:function(t,i){t&1&&m(0,et,10,0,"div",0),t&2&&u(i.open()?0:-1)},styles:["[_nghost-%COMP%]{display:contents}.backdrop[_ngcontent-%COMP%]{position:fixed;inset:0;background:#00000080;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);display:grid;place-items:start center;padding-top:12vh;z-index:100;animation:fadeIn .1s}.palette[_ngcontent-%COMP%]{width:560px;max-width:90vw;background:var(--bg-elevated);border:1px solid var(--border-strong);border-radius:12px;box-shadow:var(--shadow-pop);overflow:hidden}.palette-header[_ngcontent-%COMP%]{display:flex;justify-content:space-between;align-items:center;padding:12px 14px;border-bottom:1px solid var(--border-subtle);font-size:13px}.palette-list[_ngcontent-%COMP%]{max-height:380px;padding:6px}.item[_ngcontent-%COMP%]{display:flex;justify-content:space-between;align-items:center;width:100%;padding:8px 10px;background:transparent;border:none;border-radius:7px;cursor:pointer;color:var(--text-primary);font-family:inherit;font-size:13px;text-align:left}.item[_ngcontent-%COMP%]:hover{background:var(--bg-hover)}.label[_ngcontent-%COMP%]{flex:1}.sub[_ngcontent-%COMP%]{font-size:10.5px;color:var(--text-muted)}kbd[_ngcontent-%COMP%]{font-family:var(--font-mono);font-size:10px;color:var(--text-muted);background:var(--bg-canvas);border:1px solid var(--border-subtle);border-radius:4px;padding:2px 6px}"],changeDetection:0})};var Y=class e{es=null;notify=g(fe);router=g(D);start(){if(!(this.es||typeof EventSource>"u")){try{this.es=new EventSource("/api/events")}catch{return}this.es.onmessage=n=>{let t;try{t=JSON.parse(n.data)}catch{return}!t||t.kind!=="approval"&&t.kind!=="runDone"&&t.kind!=="drift"&&t.kind!=="reflect"||this.notify.notify(t.kind,t.title,{body:t.detail,tag:`${t.project}:${t.kind}:${t.id}`,onClick:()=>this.routeFor(t)})}}}stop(){this.es?.close(),this.es=null}routeFor(n){let t={project:n.project};n.kind==="drift"?this.router.navigate(["/drift"],{queryParams:t}):n.kind==="reflect"?this.router.navigate(["/improvements"],{queryParams:t}):this.router.navigate(["/runs",n.id],{queryParams:t})}static \u0275fac=function(t){return new(t||e)};static \u0275prov=B({token:e,factory:e.\u0275fac,providedIn:"root"})};var X=class e{_theme=g(q);_destroyRef=g(te);_router=g(D);_events=g(Y);constructor(){this._events.start()}sidebarCollapsed=w(typeof window<"u"&&window.innerWidth<1100);paletteOpen=w(!1);gChord={armed:!1,t:0};onGlobalKey(n){let t=n.metaKey||n.ctrlKey;if(t&&n.key.toLowerCase()==="k"){n.preventDefault(),this.paletteOpen.update(i=>!i);return}if(n.key==="Escape"){this.paletteOpen.set(!1);return}if(!t&&n.key.toLowerCase()==="g"&&!this.gChord.armed){let i=n.target;if(i&&/input|textarea/i.test(i.tagName))return;this.gChord.armed=!0,this.gChord.t=Date.now(),setTimeout(()=>{this.gChord.armed=!1},600);return}if(this.gChord.armed&&!t){let i=n.key.toLowerCase();i==="d"?this._router.navigate(["/dashboard"]):i==="g"?this._router.navigate(["/graph"]):i==="s"&&this._router.navigate(["/specs"]),this.gChord.armed=!1}}toggleSidebar(){this.sidebarCollapsed.update(n=>!n)}onOpenPalette(){this.paletteOpen.set(!0)}onClosePalette(){this.paletteOpen.set(!1)}static \u0275fac=function(t){return new(t||e)};static \u0275cmp=x({type:e,selectors:[["app-root"]],hostBindings:function(t,i){t&1&&b("keydown",function(_){return i.onGlobalKey(_)},oe)},decls:8,vars:2,consts:[[1,"shell"],[3,"collapsed"],[1,"main"],[3,"toggleSidebar","openPalette"],[1,"page-frame"],[3,"close","open"]],template:function(t,i){t&1&&(a(0,"div",0),p(1,"app-sidebar",1),a(2,"div",2),p(3,"app-status-strip"),a(4,"app-top-bar",3),b("toggleSidebar",function(){return i.toggleSidebar()})("openPalette",function(){return i.onOpenPalette()}),r(),a(5,"div",4),p(6,"router-outlet"),r()(),a(7,"app-command-palette",5),b("close",function(){return i.onClosePalette()}),r()()),t&2&&(o(),f("collapsed",i.sidebarCollapsed()),o(6),f("open",i.paletteOpen()))},dependencies:[le,G,K,J,U],styles:["[_nghost-%COMP%]{display:block;height:100%}.shell[_ngcontent-%COMP%]{display:flex;height:100vh;background:var(--bg-canvas);color:var(--text-primary);font-family:var(--font-ui);font-size:var(--fs-base)}.main[_ngcontent-%COMP%]{flex:1;display:flex;flex-direction:column;min-width:0;overflow:hidden}.page-frame[_ngcontent-%COMP%]{flex:1;min-height:0;display:flex;flex-direction:column;background:var(--bg-canvas)}"],changeDetection:0})};se(X,he).catch(e=>console.error(e));typeof navigator<"u"&&"serviceWorker"in navigator&&window.addEventListener("load",()=>{navigator.serviceWorker.register("sw.js").catch(e=>{console.warn("[specship] offline service worker registration failed",e)})});
1
+ import{a as q,b as fe}from"./chunk-2IVSOVQZ.js";import{a as ge}from"./chunk-YV5LB22R.js";import{b as le,c as D,d as pe,e as de,f as ce}from"./chunk-DOKZOFKM.js";import{a as me,b as ue,c as z}from"./chunk-PAO2624Y.js";import{a as I}from"./chunk-PBRCKHEQ.js";import{a as V}from"./chunk-3H2EZLZN.js";import{$ as ie,Aa as p,Ba as c,Ca as v,Da as M,E as B,Ga as P,Ha as $,I as g,Ia as b,Ja as L,Ka as d,M as C,N as y,R as te,Ua as k,V as ne,Va as s,W as w,Wa as h,Xa as A,Ya as ae,ca as oe,cb as re,da as Z,ea as o,ka as x,lb as T,ob as N,pb as H,qa as j,ra as m,sa as u,va as S,wa as O,wb as se,xa as f,ya as a,za as r,zb as R}from"./chunk-SYGIPC2P.js";import"./chunk-Q7L6LLAK.js";var ve=[{path:"",pathMatch:"full",redirectTo:"dashboard"},{path:"dashboard",loadComponent:()=>import("./chunk-7ULUC3DC.js").then(e=>e.Dashboard),data:{nav:"dashboard",title:"Dashboard"}},{path:"graph",loadComponent:()=>import("./chunk-YVAUYHOB.js").then(e=>e.Graph),data:{nav:"graph",title:"Graph"}},{path:"specs",loadComponent:()=>import("./chunk-QJSQ3ORB.js").then(e=>e.Specs),data:{nav:"specs",title:"Specs"}},{path:"specs/:id",loadComponent:()=>import("./chunk-U6CWVOZO.js").then(e=>e.SpecDetail),data:{nav:"specs",title:"Spec"}},{path:"drift",loadComponent:()=>import("./chunk-H2TR6BKW.js").then(e=>e.Drift),data:{nav:"drift",title:"Drift queue"}},{path:"improvements",loadComponent:()=>import("./chunk-BDF4LLKR.js").then(e=>e.Improvements),data:{nav:"improvements",title:"Improvements"}},{path:"maintainability",loadComponent:()=>import("./chunk-FYXVIWM2.js").then(e=>e.Maintainability),data:{nav:"maintainability",title:"Maintainability"}},{path:"domain",loadComponent:()=>import("./chunk-NY5CBIVU.js").then(e=>e.Domain),data:{nav:"domain",title:"Domain"}},{path:"workflows",loadComponent:()=>import("./chunk-KXMWAQNB.js").then(e=>e.Workflows),data:{nav:"workflows",title:"Workflows"}},{path:"runs",loadComponent:()=>import("./chunk-IXQIWN6Z.js").then(e=>e.Runs),data:{nav:"runs",title:"Runs"}},{path:"runs/:id",loadComponent:()=>import("./chunk-HZYJAXHL.js").then(e=>e.RunDetail),data:{nav:"runs",title:"Run detail"}},{path:"chat",loadComponent:()=>import("./chunk-6FZTSHJS.js").then(e=>e.Chat),data:{nav:"chat",title:"Chat"}},{path:"sessions",loadComponent:()=>import("./chunk-7ALHQYYG.js").then(e=>e.Sessions),data:{nav:"sessions",title:"Sessions"}},{path:"sessions/:id",loadComponent:()=>import("./chunk-C6I5R6UZ.js").then(e=>e.SessionDetail),data:{nav:"sessions",title:"Session detail"}},{path:"heatmap",loadComponent:()=>import("./chunk-EAWW6PQJ.js").then(e=>e.Heatmap),data:{nav:"heatmap",title:"Heatmap"}},{path:"costs",loadComponent:()=>import("./chunk-G2FTQVMJ.js").then(e=>e.Costs),data:{nav:"costs",title:"Costs"}},{path:"specship-impact",loadComponent:()=>import("./chunk-5CR7TMVN.js").then(e=>e.SpecshipImpact),data:{nav:"specship-impact",title:"SpecShip Impact"}},{path:"compare",loadComponent:()=>import("./chunk-7V4MGZCG.js").then(e=>e.Compare),data:{nav:"compare",title:"Compare projects"}},{path:"tips",loadComponent:()=>import("./chunk-D7HQMX7C.js").then(e=>e.Tips),data:{nav:"tips",title:"Tips"}},{path:"memory",loadComponent:()=>import("./chunk-YPWBUJH7.js").then(e=>e.Memory),data:{nav:"memory",title:"Memory"}},{path:"design",loadComponent:()=>import("./chunk-4G7XU55L.js").then(e=>e.Design),data:{nav:"design",title:"Design system"}},{path:"mcp",loadComponent:()=>import("./chunk-BZ5SMVXG.js").then(e=>e.Mcp),data:{nav:"mcp",title:"MCP servers"}},{path:"mcp/:id",loadComponent:()=>import("./chunk-Z23KACD6.js").then(e=>e.McpDetail),data:{nav:"mcp",title:"MCP server"}},{path:"settings",loadComponent:()=>import("./chunk-G2MHCJUR.js").then(e=>e.Settings),data:{nav:"settings",title:"Settings"}},{path:"**",redirectTo:"dashboard"}];var he={providers:[ne(),ce(ve)]};var _e=e=>["/",e],be=(e,n)=>n.group,xe=(e,n)=>n.id;function Ce(e,n){e&1&&(a(0,"div",3),s(1,"SpecShip "),a(2,"span",14),s(3,"Desktop"),r()())}function ye(e,n){if(e&1&&(a(0,"div",15),s(1),r()),e&2){let t=d().$implicit;o(),h(t.group)}}function Pe(e,n){if(e&1&&(a(0,"span",18),s(1),r()),e&2){let t=d(2).$implicit,i=d(2);j("data-kind",t.badgeKind),o(),h(i.badgeOf(t))}}function ke(e,n){if(e&1&&(a(0,"span",11),s(1),r(),m(2,Pe,2,2,"span",18)),e&2){let t=d().$implicit,i=d(2);o(),h(t.label),o(),u(i.badgeOf(t)>0?2:-1)}}function we(e,n){if(e&1&&(a(0,"a",16),p(1,"span",8),a(2,"span",9),p(3,"app-icon",17),r(),m(4,ke,3,2),r()),e&2){let t=n.$implicit,i=d(2);f("routerLink",re(5,_e,t.id))("title",i.collapsed()?t.label:null),o(3),f("name",t.icon)("size",16),o(),u(i.collapsed()?-1:4)}}function Me(e,n){if(e&1&&(a(0,"div",5),m(1,ye,2,1,"div",15),S(2,we,5,7,"a",16,xe),r()),e&2){let t=n.$implicit,i=d();o(),u(i.collapsed()?-1:1),o(),O(t.items)}}function Se(e,n){e&1&&(a(0,"span",11),s(1,"Design system"),r())}function Oe(e,n){e&1&&(a(0,"span",11),s(1,"Settings"),r())}var G=class e{collapsed=H(!1);api=g(V);projects=g(I);status=z(this.api,()=>`/api/status${this.projects.projectQuery()}`);tips=z(this.api,()=>"/api/claude/tips");improvements=z(this.api,()=>"/api/reflect?state=open");nav=[{group:"Project",items:[{id:"dashboard",label:"Dashboard",icon:"dashboard"},{id:"graph",label:"Graph",icon:"graph"},{id:"specs",label:"Specs",icon:"book"},{id:"domain",label:"Domain",icon:"layers"},{id:"drift",label:"Drift queue",icon:"drift",badge:()=>this.status.state().data?.drift??0,badgeKind:"warn"},{id:"maintainability",label:"Maintainability",icon:"wrench"},{id:"improvements",label:"Improvements",icon:"sparkles",badge:()=>(this.improvements.state().data?.proposals??[]).filter(n=>n.severity==="high").length,badgeKind:"error"},{id:"workflows",label:"Workflows",icon:"workflow"},{id:"runs",label:"Runs",icon:"play"},{id:"chat",label:"Chat",icon:"chat"}]},{group:"Claude Code",items:[{id:"sessions",label:"Sessions",icon:"sessions"},{id:"heatmap",label:"Heatmap",icon:"heatmap"},{id:"costs",label:"Costs",icon:"dollar"},{id:"specship-impact",label:"SpecShip Impact",icon:"graph"},{id:"compare",label:"Compare projects",icon:"compare"},{id:"memory",label:"Memory",icon:"memory"},{id:"mcp",label:"MCP",icon:"plug"},{id:"tips",label:"Tips",icon:"tips",badge:()=>(this.tips.state().data?.tips??[]).filter(n=>n.severity!=="info").length,badgeKind:"error"}]}];badgeOf(n){return n.badge?n.badge():0}static \u0275fac=function(t){return new(t||e)};static \u0275cmp=x({type:e,selectors:[["app-sidebar"]],inputs:{collapsed:[1,"collapsed"]},decls:18,vars:10,consts:[[1,"sidebar"],[1,"brand"],[3,"size"],[1,"brand-text"],[1,"scroll-y","nav-scroll"],[1,"nav-group"],[1,"pinned"],["routerLink","/design","routerLinkActive","active",1,"nav-item",3,"title"],[1,"indicator"],[1,"nav-icon"],["name","palette",3,"size"],[1,"nav-label"],["routerLink","/settings","routerLinkActive","active",1,"nav-item",3,"title"],["name","settings",3,"size"],[1,"muted"],[1,"eyebrow","group-label"],["routerLinkActive","active",1,"nav-item",3,"routerLink","title"],[3,"name","size"],[1,"badge"]],template:function(t,i){t&1&&(a(0,"nav",0)(1,"div",1),p(2,"app-logo-mark",2),m(3,Ce,4,0,"div",3),r(),a(4,"div",4),S(5,Me,4,1,"div",5,be),r(),a(7,"div",6)(8,"a",7),p(9,"span",8),a(10,"span",9),p(11,"app-icon",10),r(),m(12,Se,2,0,"span",11),r(),a(13,"a",12),p(14,"span",8),a(15,"span",9),p(16,"app-icon",13),r(),m(17,Oe,2,0,"span",11),r()()()),t&2&&(k("collapsed",i.collapsed()),o(2),f("size",26),o(),u(i.collapsed()?-1:3),o(2),O(i.nav),o(3),f("title",i.collapsed()?"Design system":null),o(3),f("size",16),o(),u(i.collapsed()?-1:12),o(),f("title",i.collapsed()?"Settings":null),o(3),f("size",16),o(),u(i.collapsed()?-1:17))},dependencies:[pe,de,R,ge],styles:['@charset "UTF-8";[_nghost-%COMP%]{display:contents}.sidebar[_ngcontent-%COMP%]{width:var(--sidebar-w);flex-shrink:0;background:var(--bg-panel);border-right:1px solid var(--border-subtle);display:flex;flex-direction:column;transition:width .12s;overflow:hidden;font-family:var(--font-ui)}.sidebar.collapsed[_ngcontent-%COMP%]{width:var(--sidebar-w-collapsed)}.brand[_ngcontent-%COMP%]{display:flex;align-items:center;gap:8px;padding:15px 16px}.collapsed[_ngcontent-%COMP%] .brand[_ngcontent-%COMP%]{justify-content:center;padding:14px 0}.brand-text[_ngcontent-%COMP%]{font-weight:600;font-size:13.5px;letter-spacing:-.01em}.brand-text[_ngcontent-%COMP%] .muted[_ngcontent-%COMP%]{color:var(--text-muted);font-weight:400}.nav-scroll[_ngcontent-%COMP%]{flex:1;padding:4px 8px}.nav-group[_ngcontent-%COMP%]{margin-bottom:10px}.group-label[_ngcontent-%COMP%]{padding:8px 8px 4px}.nav-item[_ngcontent-%COMP%]{position:relative;display:flex;align-items:center;gap:10px;height:32px;padding:0 10px;border-radius:7px;text-decoration:none;margin-bottom:1px;color:var(--text-secondary);font-weight:450;font-size:13px;transition:background .1s}.collapsed[_ngcontent-%COMP%] .nav-item[_ngcontent-%COMP%]{padding:0;justify-content:center}.nav-item[_ngcontent-%COMP%]:hover{background:var(--bg-hover);color:var(--text-primary)}.nav-item.active[_ngcontent-%COMP%]{background:var(--bg-active);color:var(--text-primary);font-weight:600}.nav-item.active[_ngcontent-%COMP%] .indicator[_ngcontent-%COMP%]{position:absolute;left:0;top:7px;bottom:7px;width:2.5px;border-radius:2px;background:var(--accent)}.nav-icon[_ngcontent-%COMP%]{width:16px;height:16px;flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;color:var(--text-secondary)}.nav-item[_ngcontent-%COMP%]:hover .nav-icon[_ngcontent-%COMP%]{color:var(--text-primary)}.nav-item.active[_ngcontent-%COMP%] .nav-icon[_ngcontent-%COMP%]{color:var(--accent)}.nav-label[_ngcontent-%COMP%]{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.badge[_ngcontent-%COMP%]{font-family:var(--font-mono);font-variant-numeric:tabular-nums;font-size:10px;font-weight:700;padding:1px 6px;min-width:18px;border-radius:999px;display:inline-flex;justify-content:center;color:var(--text-secondary);background:#ffffff0f}.badge[data-kind=error][_ngcontent-%COMP%]{color:var(--error);background:var(--error-soft)}.badge[data-kind=warn][_ngcontent-%COMP%]{color:var(--warn);background:var(--warn-soft)}.pinned[_ngcontent-%COMP%]{padding:8px;border-top:1px solid var(--border-subtle)}'],changeDetection:0})};function Ee(e,n){if(e&1&&(c(0,"span",8),s(1),v(),M(2,"span",1),c(3,"span",9)(4,"span",10),s(5,"nodes"),v(),c(6,"span",4),s(7),v()(),c(8,"span",9)(9,"span",10),s(10,"edges"),v(),c(11,"span",4),s(12),v()(),M(13,"span",1),c(14,"span",9)(15,"span",10),s(16,"drift"),v(),c(17,"span",4),s(18),v()(),M(19,"span",11),c(20,"span",9)(21,"span",10),s(22,"indexed"),v(),c(23,"span",4),s(24),v()()),e&2){let t=d(),i=t.status.state().data;o(),h(i.projectPath),o(6),h(i.nodeCount.toLocaleString()),o(5),h(i.edgeCount.toLocaleString()),o(5),k("warn",i.drift>0)("success",i.drift===0),o(),h(i.drift),o(6),h(t.indexedLabel())}}function je(e,n){e&1&&(c(0,"span",12),s(1,"loading status\u2026"),v(),M(2,"span",11))}function Te(e,n){if(e&1&&(c(0,"span",12),s(1),v(),M(2,"span",11)),e&2){let t=d();o(),h(t.conn.online()?"no backend":"offline \u2014 no cached data")}}function De(e,n){e&1&&(c(0,"span",5),s(1),v()),e&2&&(o(),A("\xB7 ",n))}function Ie(e,n){e&1&&(c(0,"span",6),s(1,"\u26A0"),v()),e&2&&$("title",n)}var K=class e{api=g(V);projects=g(I);refreshSvc=g(ue);conn=g(me);status=z(this.api,()=>`/api/status${this.projects.projectQuery()}`);offlineAge=T(()=>{let n=this.status.state().cachedAt??this.conn.lastOnlineAt();if(n===null)return null;let t=Date.now()-n;return t<1e4?"just now":t<6e4?`${Math.round(t/1e3)}s ago`:t<36e5?`${Math.round(t/6e4)}m ago`:t<864e5?`${Math.round(t/36e5)}h ago`:`${Math.round(t/864e5)}d ago`});indexedLabel=T(()=>{let n=this.status.state().data?.lastIndexed;if(!n)return"never";try{let t=new Date(n),i=Date.now()-t.getTime();return i<6e4?"just now":i<36e5?`${Math.round(i/6e4)}m ago`:i<864e5?`${Math.round(i/36e5)}h ago`:t.toLocaleDateString()}catch{return String(n)}});async refresh(){await this.refreshSvc.triggerGlobalRefresh()}static \u0275fac=function(t){return new(t||e)};static \u0275cmp=x({type:e,selectors:[["app-status-strip"]],decls:13,vars:14,consts:[[1,"status-strip"],[1,"divider"],[1,"seg","conn",3,"title"],[1,"conn-dot"],[1,"mono","val"],[1,"muted","conn-age"],[1,"refresh-error","mono",3,"title"],[1,"refresh",3,"click","disabled","title"],[1,"seg","mono"],[1,"seg"],[1,"lbl"],[1,"grow"],[1,"mono","muted"]],template:function(t,i){if(t&1&&(c(0,"div",0),m(1,Ee,25,9)(2,je,3,0)(3,Te,3,1),M(4,"span",1),c(5,"span",2),M(6,"span",3),c(7,"span",4),s(8),v(),m(9,De,2,1,"span",5),v(),m(10,Ie,2,1,"span",6),c(11,"button",7),L("click",function(){return i.refresh()}),s(12,"\u21BB"),v()()),t&2){let l,_;o(),u(i.status.state().data?1:i.status.state().loading?2:3),o(4),$("title",i.conn.online()?"Connected to the SpecShip server":"Server unreachable \u2014 showing the last cached data"),o(),k("live",i.conn.online())("off",!i.conn.online()),o(2),h(i.conn.online()?"Live":"Offline"),o(),u((l=!i.conn.online()&&i.offlineAge())?9:-1,l),o(),u((_=i.refreshSvc.error())?10:-1,_),o(),k("spinning",i.refreshSvc.loading()),$("disabled",i.refreshSvc.loading()||!i.conn.online())("title",i.conn.online()?i.refreshSvc.loading()?"Refreshing\u2026":"Refresh: sync index + re-ingest Claude Code transcripts. Updates Sessions, Heatmap, Costs, Memory, Drift, Graph.":"Offline \u2014 reconnect to refresh"),j("aria-label",i.conn.online()?i.refreshSvc.loading()?"Refreshing\u2026":"Refresh everything \u2014 sync index + ingest Claude Code transcripts":"Offline \u2014 reconnect to refresh")}},styles:["[_nghost-%COMP%]{display:contents}.status-strip[_ngcontent-%COMP%]{height:var(--status-h);flex-shrink:0;display:flex;align-items:center;gap:14px;padding:0 14px;border-bottom:1px solid var(--border-subtle);background:var(--bg-panel-2);font-size:11.5px;overflow:hidden;white-space:nowrap}.seg[_ngcontent-%COMP%]{display:flex;align-items:center;gap:6px;flex-shrink:0}.lbl[_ngcontent-%COMP%]{color:var(--text-muted);font-size:11px}.val[_ngcontent-%COMP%]{color:var(--text-secondary);font-variant-numeric:tabular-nums}.val.warn[_ngcontent-%COMP%]{color:var(--warn)}.val.success[_ngcontent-%COMP%]{color:var(--success)}.divider[_ngcontent-%COMP%]{width:1px;height:12px;background:var(--border-subtle);flex-shrink:0}.grow[_ngcontent-%COMP%]{flex:1}.muted[_ngcontent-%COMP%]{color:var(--text-muted)}.refresh[_ngcontent-%COMP%]{background:transparent;border:none;color:var(--text-muted);cursor:pointer;font-size:14px;padding:2px 6px;border-radius:4px;transition:background .12s,color .12s}.refresh[_ngcontent-%COMP%]:hover:not(:disabled){background:var(--bg-hover);color:var(--text-primary)}.refresh[_ngcontent-%COMP%]:disabled{cursor:progress;opacity:.8}.refresh[_ngcontent-%COMP%]:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.refresh.spinning[_ngcontent-%COMP%]{display:inline-block;animation:_ngcontent-%COMP%_spin .9s linear infinite;color:var(--accent)}@keyframes _ngcontent-%COMP%_spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.refresh-error[_ngcontent-%COMP%]{color:var(--warn);font-size:12px;padding:0 2px;cursor:help}.conn[_ngcontent-%COMP%]{gap:5px}.conn-dot[_ngcontent-%COMP%]{width:6px;height:6px;border-radius:50%;flex-shrink:0}.conn-dot.live[_ngcontent-%COMP%]{background:var(--success);box-shadow:0 0 6px var(--success)}.conn-dot.off[_ngcontent-%COMP%]{background:var(--warn);box-shadow:0 0 6px var(--warn)}.conn-age[_ngcontent-%COMP%]{font-size:11px}"],changeDetection:0})};var ze=(e,n)=>n.slug;function Re(e,n){e&1&&p(0,"span",8)}function Le(e,n){e&1&&p(0,"span",9)}function Ae(e,n){e&1&&p(0,"span",10)}function Ne(e,n){if(e&1&&(a(0,"span",7),s(1),r(),m(2,Re,1,0,"span",8)(3,Le,1,0,"span",9)(4,Ae,1,0,"span",10)),e&2){let t=n,i=d();o(),h(i.shortPath(t.path)),o(),u(t.exists?t.initialized?4:3:2)}}function Fe(e,n){e&1&&(a(0,"span",3),s(1,"Select project"),r())}function Be(e,n){if(e&1){let t=P();a(0,"button",20),b("click",function(){C(t);let l=d(2);return y(l.clear())}),p(1,"app-icon",21),s(2," Clear "),r()}e&2&&(o(),f("size",11))}function $e(e,n){e&1&&(a(0,"span",16),s(1,"\xB7 loading\u2026"),r())}function He(e,n){if(e&1&&(a(0,"span",16),s(1),r()),e&2){let t=d(2);o(),A("\xB7 ",t.filtered().length," shown \xB7 live")}}function Ve(e,n){e&1&&(a(0,"div",18),s(1,"No projects match."),r())}function qe(e,n){e&1&&(a(0,"span",25),s(1,"missing"),r())}function Ge(e,n){e&1&&(a(0,"span",26),s(1,"not init"),r())}function Ke(e,n){e&1&&(a(0,"span",27),s(1,"initialized"),r())}function We(e,n){if(e&1&&(a(0,"span"),s(1),r()),e&2){let t=d().$implicit,i=d(2);o(),A("\xB7 ",i.formatRelative(t.lastModifiedMs))}}function Qe(e,n){e&1&&p(0,"app-icon",29),e&2&&f("size",13)}function Je(e,n){if(e&1){let t=P();a(0,"button",22),b("click",function(){let l=C(t).$implicit,_=d(2);return y(_.pick(l.slug))}),a(1,"div",23)(2,"div",24)(3,"span",7),s(4),r(),m(5,qe,2,0,"span",25)(6,Ge,2,0,"span",26)(7,Ke,2,0,"span",27),r(),a(8,"div",28)(9,"span"),s(10),r(),m(11,We,2,1,"span"),r()(),m(12,Qe,1,1,"app-icon",29),r()}if(e&2){let t=n.$implicit,i=d(2);k("active",i.projects.activeSlug()===t.slug),o(4),h(i.shortPath(t.path)),o(),u(t.exists?t.initialized?7:6:5),o(5),ae("",t.sessionCount," session",t.sessionCount===1?"":"s"),o(),u(t.lastModifiedMs>0?11:-1),o(),u(i.projects.activeSlug()===t.slug?12:-1)}}function Ue(e,n){if(e&1){let t=P();a(0,"div",6)(1,"div",11),p(2,"app-icon",12),a(3,"input",13),b("input",function(l){C(t);let _=d();return y(_.onQuery(l.target.value))}),r(),m(4,Be,3,1,"button",14),r(),a(5,"div",15)(6,"span"),s(7),r(),m(8,$e,2,0,"span",16)(9,He,2,1,"span",16),r(),a(10,"div",17),m(11,Ve,2,0,"div",18),S(12,Je,13,8,"button",19,ze),r()()}if(e&2){let t=d();o(2),f("size",13),o(),f("value",t.query()),o(),u(t.projects.activeSlug()?4:-1),o(3),h(t.projects.claudeRoot()??"~/.claude/projects"),o(),u(t.projects.loading()?8:9),o(3),u(t.filtered().length===0&&!t.projects.loading()?11:-1),o(),O(t.filtered())}}var W=class e{host=g(ie);projects=g(I);open=w(!1);query=w("");filtered=T(()=>{let n=this.query().trim().toLowerCase(),t=this.projects.projects();return n?t.filter(i=>i.path.toLowerCase().includes(n)||i.slug.toLowerCase().includes(n)):t});badge=T(()=>this.projects.projects().length);toggle(){this.open.update(n=>!n)}pick(n){this.projects.setActive(n),this.open.set(!1),this.query.set("")}clear(){this.projects.setActive(null),this.open.set(!1)}onQuery(n){this.query.set(n)}onDocumentClick(n){if(!this.open())return;let t=n.target;t&&!this.host.nativeElement.contains(t)&&this.open.set(!1)}onEscape(){this.open()&&this.open.set(!1)}formatRelative(n){if(!n)return"";let t=Date.now()-n,i=Math.round(t/1e3);if(i<60)return`${i}s ago`;let l=Math.round(i/60);if(l<60)return`${l}m ago`;let _=Math.round(l/60);if(_<24)return`${_}h ago`;let ee=Math.round(_/24);return ee<30?`${ee}d ago`:new Date(n).toLocaleDateString()}shortPath(n){if(!n)return"";let t=this.detectHome();return t&&n===t?"~":t&&n.startsWith(t+"/")?"~/"+n.slice(t.length+1):n}detectHome(){let n=this.projects.claudeRoot();if(!n)return null;let t="/.claude/projects";return n.endsWith(t)?n.slice(0,-t.length):null}static \u0275fac=function(t){return new(t||e)};static \u0275cmp=x({type:e,selectors:[["app-project-picker"]],hostBindings:function(t,i){t&1&&b("click",function(_){return i.onDocumentClick(_)},Z)("keydown.escape",function(){return i.onEscape()},Z)},decls:9,vars:9,consts:[[1,"wrap"],["type","button","aria-label","Switch project",1,"trigger",3,"click"],["name","folder",3,"size"],[1,"placeholder"],[1,"badge","mono"],[3,"name","size"],["role","dialog","aria-label","Pick a project",1,"popover"],[1,"path","mono"],["title","path no longer exists on disk",1,"status-dot","missing"],["title","not yet specship init'd",1,"status-dot","warn"],["title","initialized",1,"status-dot","ok"],[1,"popover-head","row","gap-8"],["name","search",3,"size"],["type","text","placeholder","Filter projects\u2026","autofocus","",1,"popover-input",3,"input","value"],["title","Clear selection",1,"btn","btn-ghost","btn-xs"],[1,"popover-meta","mono"],[1,"muted"],[1,"popover-list","scroll-y"],[1,"empty"],["type","button",1,"row-item",3,"active"],["title","Clear selection",1,"btn","btn-ghost","btn-xs",3,"click"],["name","x",3,"size"],["type","button",1,"row-item",3,"click"],[1,"col","gap-2","grow"],[1,"row","gap-6","path-line"],[1,"pill","missing"],[1,"pill","warn"],[1,"pill","ok"],[1,"meta","row","gap-8","mono","muted"],["name","check",3,"size"]],template:function(t,i){if(t&1&&(a(0,"div",0)(1,"button",1),b("click",function(){return i.toggle()}),p(2,"app-icon",2),m(3,Ne,5,2)(4,Fe,2,0,"span",3),a(5,"span",4),s(6),r(),p(7,"app-icon",5),r(),m(8,Ue,14,6,"div",6),r()),t&2){let l;k("open",i.open()),o(),j("aria-expanded",i.open()),o(),f("size",13),o(),u((l=i.projects.active())?3:4,l),o(3),h(i.badge()),o(),f("name",i.open()?"chevronDown":"chevronRight")("size",11),o(),u(i.open()?8:-1)}},dependencies:[R],styles:["[_nghost-%COMP%]{display:inline-flex;min-width:0}.wrap[_ngcontent-%COMP%]{position:relative;min-width:0}.trigger[_ngcontent-%COMP%]{height:30px;padding:0 11px 0 10px;background:var(--bg-panel);border:1px solid var(--border-subtle);border-radius:8px;color:var(--text-secondary);display:inline-flex;align-items:center;gap:8px;font-family:var(--font-ui);font-size:12.5px;cursor:pointer;max-width:360px;min-width:0;transition:background .1s,border-color .1s}.trigger[_ngcontent-%COMP%]:hover, .wrap.open[_ngcontent-%COMP%] .trigger[_ngcontent-%COMP%]{background:var(--bg-panel-2);border-color:var(--border-strong);color:var(--text-primary)}.trigger[_ngcontent-%COMP%] app-icon[_ngcontent-%COMP%]{color:var(--text-muted);flex-shrink:0}.trigger[_ngcontent-%COMP%]:hover app-icon[_ngcontent-%COMP%]{color:var(--text-secondary)}.path[_ngcontent-%COMP%]{font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.placeholder[_ngcontent-%COMP%]{color:var(--text-muted);font-style:italic}.status-dot[_ngcontent-%COMP%]{width:6px;height:6px;border-radius:50%;flex-shrink:0}.status-dot.ok[_ngcontent-%COMP%]{background:var(--success)}.status-dot.warn[_ngcontent-%COMP%]{background:var(--warn)}.status-dot.missing[_ngcontent-%COMP%]{background:var(--error)}.badge[_ngcontent-%COMP%]{font-size:10px;padding:1px 6px;border-radius:999px;background:var(--bg-active);color:var(--text-muted);font-variant-numeric:tabular-nums;flex-shrink:0}.popover[_ngcontent-%COMP%]{position:absolute;top:calc(100% + 6px);left:0;z-index:30;width:420px;max-width:calc(100vw - 40px);background:var(--bg-elevated);border:1px solid var(--border-strong);border-radius:10px;box-shadow:var(--shadow-pop);display:flex;flex-direction:column;max-height:480px;animation:_ngcontent-%COMP%_pp-in .12s ease-out}@keyframes _ngcontent-%COMP%_pp-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.popover-head[_ngcontent-%COMP%]{padding:10px 12px 8px;border-bottom:1px solid var(--border-subtle)}.popover-head[_ngcontent-%COMP%] app-icon[_ngcontent-%COMP%]{color:var(--text-muted);flex-shrink:0}.popover-input[_ngcontent-%COMP%]{flex:1;min-width:0;background:transparent;border:none;outline:none;font-family:var(--font-ui);font-size:13px;color:var(--text-primary)}.popover-input[_ngcontent-%COMP%]::placeholder{color:var(--text-muted)}.popover-meta[_ngcontent-%COMP%]{padding:6px 12px 8px;font-size:10.5px;color:var(--text-secondary);border-bottom:1px solid var(--border-subtle);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.popover-meta[_ngcontent-%COMP%] .muted[_ngcontent-%COMP%]{color:var(--text-muted)}.popover-list[_ngcontent-%COMP%]{flex:1;min-height:0;padding:4px}.row-item[_ngcontent-%COMP%]{width:100%;display:flex;align-items:center;gap:10px;padding:9px 10px;border:none;border-radius:7px;background:transparent;color:inherit;cursor:pointer;font-family:inherit;text-align:left;transition:background 80ms}.row-item[_ngcontent-%COMP%]:hover{background:var(--bg-hover)}.row-item.active[_ngcontent-%COMP%]{background:var(--accent-soft);color:var(--text-primary)}.row-item.active[_ngcontent-%COMP%] app-icon[_ngcontent-%COMP%]{color:var(--accent)}.path-line[_ngcontent-%COMP%]{min-width:0}.path-line[_ngcontent-%COMP%] .path[_ngcontent-%COMP%]{font-size:12.5px;font-weight:500;color:var(--text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.meta[_ngcontent-%COMP%]{font-size:10.5px}.pill[_ngcontent-%COMP%]{font-size:10px;font-weight:600;padding:1px 7px;border-radius:999px;white-space:nowrap;flex-shrink:0}.pill.ok[_ngcontent-%COMP%]{color:var(--success);background:var(--success-soft)}.pill.warn[_ngcontent-%COMP%]{color:var(--warn);background:var(--warn-soft)}.pill.missing[_ngcontent-%COMP%]{color:var(--error);background:var(--error-soft)}.empty[_ngcontent-%COMP%]{text-align:center;color:var(--text-muted);font-size:12px;padding:18px 12px}.muted[_ngcontent-%COMP%]{color:var(--text-muted)}.mono[_ngcontent-%COMP%]{font-family:var(--font-mono)}.grow[_ngcontent-%COMP%]{flex:1;min-width:0}.row[_ngcontent-%COMP%]{display:flex;align-items:center}.col[_ngcontent-%COMP%]{display:flex;flex-direction:column}.gap-2[_ngcontent-%COMP%]{gap:2px}.gap-6[_ngcontent-%COMP%]{gap:6px}.gap-8[_ngcontent-%COMP%]{gap:8px}.scroll-y[_ngcontent-%COMP%]{overflow-y:auto;overflow-x:hidden}"],changeDetection:0})};var Q=class e{deferred=null;canInstall=w(!1);constructor(){typeof window>"u"||window.matchMedia?.("(display-mode: standalone)").matches===!0||window.navigator.standalone===!0||(window.addEventListener("beforeinstallprompt",t=>{t.preventDefault(),this.deferred=t,this.canInstall.set(!0)}),window.addEventListener("appinstalled",()=>{this.deferred=null,this.canInstall.set(!1)}))}async install(){let n=this.deferred;if(n){this.deferred=null,this.canInstall.set(!1);try{await n.prompt(),await n.userChoice}catch{}}}static \u0275fac=function(t){return new(t||e)};static \u0275prov=B({token:e,factory:e.\u0275fac,providedIn:"root"})};function Ye(e,n){if(e&1){let t=P();a(0,"button",9),b("click",function(){C(t);let l=d();return y(l.onInstall())}),p(1,"app-icon",10),a(2,"span"),s(3,"Install"),r()()}e&2&&(o(),f("size",14))}var J=class e{toggleSidebar=N();openPalette=N();theme=g(q);effective=this.theme.effective;pwa=g(Q);onToggleTheme(){this.theme.toggle()}onMenuClick(){this.toggleSidebar.emit()}onPalette(){this.openPalette.emit()}onInstall(){this.pwa.install()}static \u0275fac=function(t){return new(t||e)};static \u0275cmp=x({type:e,selectors:[["app-top-bar"]],outputs:{toggleSidebar:"toggleSidebar",openPalette:"openPalette"},decls:14,vars:6,consts:[[1,"topbar"],["title","Toggle sidebar","aria-label","Toggle sidebar",1,"ghost","icon-btn",3,"click"],["name","menu",3,"size"],["aria-label","Open command palette",1,"palette-trigger",3,"click"],["name","search",3,"size"],[1,"grow"],["title","Install SpecShip as an app","aria-label","Install SpecShip as an app",1,"ghost",2,"display","inline-flex","align-items","center","gap","6px","padding","0 10px","height","30px","font-size","12px"],["aria-label","Toggle theme",1,"ghost","icon-btn",3,"click","title"],[3,"name","size"],["title","Install SpecShip as an app","aria-label","Install SpecShip as an app",1,"ghost",2,"display","inline-flex","align-items","center","gap","6px","padding","0 10px","height","30px","font-size","12px",3,"click"],["name","plus",3,"size"]],template:function(t,i){t&1&&(a(0,"div",0)(1,"button",1),b("click",function(){return i.onMenuClick()}),p(2,"app-icon",2),r(),p(3,"app-project-picker"),a(4,"button",3),b("click",function(){return i.onPalette()}),p(5,"app-icon",4),a(6,"span"),s(7,"Search or jump to\u2026"),r(),a(8,"kbd"),s(9,"\u2318K"),r()(),p(10,"span",5),m(11,Ye,4,1,"button",6),a(12,"button",7),b("click",function(){return i.onToggleTheme()}),p(13,"app-icon",8),r()()),t&2&&(o(2),f("size",16),o(3),f("size",13),o(6),u(i.pwa.canInstall()?11:-1),o(),f("title","Theme: "+i.effective()),o(),f("name",i.effective()==="dark"?"sun":"moon")("size",16))},dependencies:[R,W],styles:["[_nghost-%COMP%]{display:contents}.topbar[_ngcontent-%COMP%]{height:44px;flex-shrink:0;display:flex;align-items:center;gap:10px;padding:0 14px;border-bottom:1px solid var(--border-subtle);background:var(--bg-canvas)}.ghost[_ngcontent-%COMP%]{background:transparent;border:1px solid transparent;color:var(--text-secondary);cursor:pointer}.ghost[_ngcontent-%COMP%]:hover{background:var(--bg-hover);color:var(--text-primary)}.icon-btn[_ngcontent-%COMP%]{padding:6px 9px;border-radius:5px;font-size:16px}.palette-trigger[_ngcontent-%COMP%]{flex:1;max-width:380px;height:30px;padding:0 11px;background:var(--bg-panel);border:1px solid var(--border-subtle);border-radius:8px;cursor:text;color:var(--text-muted);font-size:12.5px;text-align:left;display:flex;align-items:center;gap:8px}.palette-trigger[_ngcontent-%COMP%]:hover{border-color:var(--border-strong);color:var(--text-secondary)}.palette-trigger[_ngcontent-%COMP%] app-icon[_ngcontent-%COMP%]{color:var(--text-muted)}.palette-trigger[_ngcontent-%COMP%] span[_ngcontent-%COMP%]{flex:1}kbd[_ngcontent-%COMP%]{font-family:var(--font-mono);font-size:10px;color:var(--text-muted);background:var(--bg-canvas);border:1px solid var(--border-subtle);border-radius:4px;padding:2px 6px}.grow[_ngcontent-%COMP%]{flex:1}"],changeDetection:0})};var Xe=(e,n)=>n.id;function Ze(e,n){if(e&1){let t=P();c(0,"button",6),L("click",function(){let l=C(t).$implicit,_=d(2);return y(_.go(l.id))}),c(1,"span",7),s(2),v(),c(3,"span",8),s(4),v()()}if(e&2){let t=n.$implicit;o(2),h(t.label),o(2),h(t.sub)}}function et(e,n){if(e&1){let t=P();c(0,"div",1),L("click",function(l){C(t);let _=d();return y(_.onBackdrop(l))}),c(1,"div",2)(2,"div",3)(3,"span"),s(4,"Jump to\u2026"),v(),c(5,"kbd"),s(6,"esc"),v()(),c(7,"div",4),S(8,Ze,5,2,"button",5,Xe),v()()()}if(e&2){let t=d();o(8),O(t.items)}}var tt=[{id:"dashboard",label:"Dashboard",sub:"/dashboard"},{id:"graph",label:"Graph",sub:"/graph"},{id:"specs",label:"Specs",sub:"/specs"},{id:"drift",label:"Drift queue",sub:"/drift"},{id:"workflows",label:"Workflows",sub:"/workflows"},{id:"runs",label:"Runs",sub:"/runs"},{id:"chat",label:"Chat",sub:"/chat"},{id:"sessions",label:"Sessions",sub:"/sessions"},{id:"heatmap",label:"Heatmap",sub:"/heatmap"},{id:"costs",label:"Costs",sub:"/costs"},{id:"compare",label:"Compare projects",sub:"/compare"},{id:"tips",label:"Tips",sub:"/tips"},{id:"settings",label:"Settings",sub:"/settings"}],U=class e{open=H(!1);close=N();router=g(D);items=tt;go(n){this.router.navigate(["/"+n]),this.close.emit()}onBackdrop(n){n.target===n.currentTarget&&this.close.emit()}static \u0275fac=function(t){return new(t||e)};static \u0275cmp=x({type:e,selectors:[["app-command-palette"]],inputs:{open:[1,"open"]},outputs:{close:"close"},decls:1,vars:1,consts:[[1,"backdrop"],[1,"backdrop",3,"click"],[1,"palette"],[1,"palette-header"],[1,"palette-list","scroll-y"],[1,"item"],[1,"item",3,"click"],[1,"label"],[1,"sub","mono"]],template:function(t,i){t&1&&m(0,et,10,0,"div",0),t&2&&u(i.open()?0:-1)},styles:["[_nghost-%COMP%]{display:contents}.backdrop[_ngcontent-%COMP%]{position:fixed;inset:0;background:#00000080;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);display:grid;place-items:start center;padding-top:12vh;z-index:100;animation:fadeIn .1s}.palette[_ngcontent-%COMP%]{width:560px;max-width:90vw;background:var(--bg-elevated);border:1px solid var(--border-strong);border-radius:12px;box-shadow:var(--shadow-pop);overflow:hidden}.palette-header[_ngcontent-%COMP%]{display:flex;justify-content:space-between;align-items:center;padding:12px 14px;border-bottom:1px solid var(--border-subtle);font-size:13px}.palette-list[_ngcontent-%COMP%]{max-height:380px;padding:6px}.item[_ngcontent-%COMP%]{display:flex;justify-content:space-between;align-items:center;width:100%;padding:8px 10px;background:transparent;border:none;border-radius:7px;cursor:pointer;color:var(--text-primary);font-family:inherit;font-size:13px;text-align:left}.item[_ngcontent-%COMP%]:hover{background:var(--bg-hover)}.label[_ngcontent-%COMP%]{flex:1}.sub[_ngcontent-%COMP%]{font-size:10.5px;color:var(--text-muted)}kbd[_ngcontent-%COMP%]{font-family:var(--font-mono);font-size:10px;color:var(--text-muted);background:var(--bg-canvas);border:1px solid var(--border-subtle);border-radius:4px;padding:2px 6px}"],changeDetection:0})};var Y=class e{es=null;notify=g(fe);router=g(D);start(){if(!(this.es||typeof EventSource>"u")){try{this.es=new EventSource("/api/events")}catch{return}this.es.onmessage=n=>{let t;try{t=JSON.parse(n.data)}catch{return}!t||t.kind!=="approval"&&t.kind!=="runDone"&&t.kind!=="drift"&&t.kind!=="reflect"||this.notify.notify(t.kind,t.title,{body:t.detail,tag:`${t.project}:${t.kind}:${t.id}`,onClick:()=>this.routeFor(t)})}}}stop(){this.es?.close(),this.es=null}routeFor(n){let t={project:n.project};n.kind==="drift"?this.router.navigate(["/drift"],{queryParams:t}):n.kind==="reflect"?this.router.navigate(["/improvements"],{queryParams:t}):this.router.navigate(["/runs",n.id],{queryParams:t})}static \u0275fac=function(t){return new(t||e)};static \u0275prov=B({token:e,factory:e.\u0275fac,providedIn:"root"})};var X=class e{_theme=g(q);_destroyRef=g(te);_router=g(D);_events=g(Y);constructor(){this._events.start()}sidebarCollapsed=w(typeof window<"u"&&window.innerWidth<1100);paletteOpen=w(!1);gChord={armed:!1,t:0};onGlobalKey(n){let t=n.metaKey||n.ctrlKey;if(t&&n.key.toLowerCase()==="k"){n.preventDefault(),this.paletteOpen.update(i=>!i);return}if(n.key==="Escape"){this.paletteOpen.set(!1);return}if(!t&&n.key.toLowerCase()==="g"&&!this.gChord.armed){let i=n.target;if(i&&/input|textarea/i.test(i.tagName))return;this.gChord.armed=!0,this.gChord.t=Date.now(),setTimeout(()=>{this.gChord.armed=!1},600);return}if(this.gChord.armed&&!t){let i=n.key.toLowerCase();i==="d"?this._router.navigate(["/dashboard"]):i==="g"?this._router.navigate(["/graph"]):i==="s"&&this._router.navigate(["/specs"]),this.gChord.armed=!1}}toggleSidebar(){this.sidebarCollapsed.update(n=>!n)}onOpenPalette(){this.paletteOpen.set(!0)}onClosePalette(){this.paletteOpen.set(!1)}static \u0275fac=function(t){return new(t||e)};static \u0275cmp=x({type:e,selectors:[["app-root"]],hostBindings:function(t,i){t&1&&b("keydown",function(_){return i.onGlobalKey(_)},oe)},decls:8,vars:2,consts:[[1,"shell"],[3,"collapsed"],[1,"main"],[3,"toggleSidebar","openPalette"],[1,"page-frame"],[3,"close","open"]],template:function(t,i){t&1&&(a(0,"div",0),p(1,"app-sidebar",1),a(2,"div",2),p(3,"app-status-strip"),a(4,"app-top-bar",3),b("toggleSidebar",function(){return i.toggleSidebar()})("openPalette",function(){return i.onOpenPalette()}),r(),a(5,"div",4),p(6,"router-outlet"),r()(),a(7,"app-command-palette",5),b("close",function(){return i.onClosePalette()}),r()()),t&2&&(o(),f("collapsed",i.sidebarCollapsed()),o(6),f("open",i.paletteOpen()))},dependencies:[le,G,K,J,U],styles:["[_nghost-%COMP%]{display:block;height:100%}.shell[_ngcontent-%COMP%]{display:flex;height:100vh;background:var(--bg-canvas);color:var(--text-primary);font-family:var(--font-ui);font-size:var(--fs-base)}.main[_ngcontent-%COMP%]{flex:1;display:flex;flex-direction:column;min-width:0;overflow:hidden}.page-frame[_ngcontent-%COMP%]{flex:1;min-height:0;display:flex;flex-direction:column;background:var(--bg-canvas)}"],changeDetection:0})};se(X,he).catch(e=>console.error(e));typeof navigator<"u"&&"serviceWorker"in navigator&&window.addEventListener("load",()=>{navigator.serviceWorker.register("sw.js").catch(e=>{console.warn("[specship] offline service worker registration failed",e)})});
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specship/specship",
3
- "version": "0.11.5",
3
+ "version": "0.11.6",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
package/lib/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specship/specship",
3
- "version": "0.11.5",
3
+ "version": "0.11.6",
4
4
  "description": "Supercharge Claude Code with semantic code intelligence. 94% fewer tool calls • 77% faster exploration • 100% local.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specship/specship-win32-arm64",
3
- "version": "0.11.5",
3
+ "version": "0.11.6",
4
4
  "description": "SpecShip self-contained bundle for win32-arm64",
5
5
  "os": [
6
6
  "win32"
@@ -1,3 +0,0 @@
1
- import{a as L}from"./chunk-JLOJCDIY.js";import{a as j}from"./chunk-YV5LB22R.js";import{a as N}from"./chunk-IOB4JJT4.js";import{a as V}from"./chunk-3H2EZLZN.js";import{Aa as c,Ga as v,I as k,Ia as u,Ka as s,M as _,N as f,Ra as P,Sa as D,Ta as I,Ua as $,Va as l,W as b,Wa as h,Xa as C,aa as M,ea as i,ka as z,lb as T,qa as S,qb as A,ra as x,sa as g,ta as E,ua as O,va as y,wa as w,xa as d,ya as o,yb as F,za as a,zb as H}from"./chunk-SYGIPC2P.js";import"./chunk-Q7L6LLAK.js";var W=["listEl"],q=(n,t)=>t.name,G=(n,t)=>t.cmd;function U(n,t){if(n&1&&(o(0,"div",22),c(1,"div",24),a()),n&2){let e=s().$implicit,r=s();i(),d("innerHTML",r.renderMd(e.text),M)}}function J(n,t){if(n&1&&(o(0,"span",28),l(1),a()),n&2){let e=s(2).$implicit;i(),h(e.model)}}function X(n,t){if(n&1&&(o(0,"div",40)(1,"div",41),l(2,"output"),a(),o(3,"div",42),l(4),a()()),n&2){let e=s().$implicit;i(4),h(e.output)}}function Y(n,t){if(n&1){let e=v();o(0,"div",32)(1,"button",33),u("click",function(){let p=_(e).$implicit,m=s(3).$implicit,Q=s();return f(Q.toggleToolCall(m,p))}),c(2,"app-icon",34)(3,"app-icon",35),o(4,"span",36),l(5),o(6,"span",37),l(7),a()(),o(8,"span",38),c(9,"app-icon",39),l(10),a()(),x(11,X,5,1,"div",40),a()}if(n&2){let e=t.$implicit;i(),S("aria-expanded",e.open),i(),d("name",e.open?"chevronDown":"chevronRight")("size",12),i(),d("size",12),i(2),C(" ",e.name),i(2),C("(",e.input,")"),i(2),d("size",9),i(),C(" ",e.status," "),i(),g(e.open?11:-1)}}function Z(n,t){if(n&1&&y(0,Y,12,9,"div",32,q),n&2){let e=s(2).$implicit;w(e.tools)}}function ee(n,t){if(n&1&&(o(0,"div",31),c(1,"app-icon",43),l(2),o(3,"span"),l(4,"\xB7"),a(),l(5),a()),n&2){let e=s(2).$implicit,r=s();i(),d("size",11),i(),C(" $",e.cost.toFixed(2)," "),i(3),C(" ",r.fmtK(e.tokens)," tokens ")}}function te(n,t){if(n&1&&(o(0,"div",23)(1,"div",25),c(2,"app-logo-mark",26),o(3,"span",27),l(4,"specship"),a(),x(5,J,2,1,"span",28),a(),o(6,"div",29),c(7,"div",30),x(8,Z,2,0),x(9,ee,6,3,"div",31),a()()),n&2){let e=s().$implicit,r=s();i(2),d("size",22)("glow",!1),i(3),g(e.model?5:-1),i(2),d("innerHTML",r.renderMd(e.text),M),i(),g(e.tools!=null&&e.tools.length?8:-1),i(),g(e.cost!=null?9:-1)}}function ne(n,t){if(n&1&&x(0,U,2,1,"div",22)(1,te,10,6,"div",23),n&2){let e=t.$implicit;g(e.role==="user"?0:1)}}function ie(n,t){n&1&&(o(0,"div",5),c(1,"span",44),l(2," thinking with specship context\u2026 "),a())}function oe(n,t){if(n&1&&(o(0,"span",48),l(1),a()),n&2){let e=s().$implicit;i(),h(e.arg)}}function ae(n,t){if(n&1){let e=v();o(0,"button",46),u("click",function(){let p=_(e).$implicit,m=s(2);return f(m.pickSlash(p.cmd))}),o(1,"span",47),l(2),a(),x(3,oe,2,1,"span",48),o(4,"span",49),l(5),a()()}if(n&2){let e=t.$implicit;i(2),h(e.cmd),i(),g(e.arg?3:-1),i(2),h(e.desc)}}function re(n,t){if(n&1&&(o(0,"div",8),y(1,ae,6,3,"button",45,G),a()),n&2){let e=s();i(),w(e.filteredSlash())}}function le(n,t){if(n&1){let e=v();o(0,"div",20)(1,"button",50),u("click",function(){_(e);let p=s();return f(p.toggleContext())}),c(2,"app-icon",51),a()()}n&2&&(i(2),d("size",14))}function se(n,t){if(n&1){let e=v();o(0,"div",66)(1,"button",70),u("click",function(){let p=_(e).$implicit,m=s(2);return f(m.toggleTool(p))}),c(2,"span",71),a(),o(3,"span",72),l(4),a()()}if(n&2){let e=t.$implicit,r=s(2);i(),$("on",r.enabledTools().has(e)),S("aria-pressed",r.enabledTools().has(e))("aria-label",e+(r.enabledTools().has(e)?" enabled":" disabled")),i(2),I("color",r.enabledTools().has(e)?"var(--text-primary)":"var(--text-muted)"),i(),h(e)}}function de(n,t){if(n&1){let e=v();o(0,"aside",21)(1,"div",52)(2,"span",53),l(3,"Context"),a(),o(4,"button",54),u("click",function(){_(e);let p=s();return f(p.toggleContext())}),c(5,"app-icon",55),a()(),o(6,"div",56)(7,"div",57),c(8,"app-icon",58),o(9,"div",59)(10,"div",60),l(11,"Project"),a(),o(12,"div",61),l(13,"~/dev/specship"),a()()(),o(14,"div",57),c(15,"app-icon",62),o(16,"div",59)(17,"div",60),l(18,"Indexed files"),a(),o(19,"div",63),l(20),a()()(),o(21,"div",57),c(22,"app-icon",64),o(23,"div",59)(24,"div",60),l(25,"Drift queue"),a(),o(26,"div",63),l(27,"\u2013 links"),a()()(),o(28,"div",65),l(29,"MCP tools"),a(),y(30,se,5,7,"div",66,O),a(),o(32,"div",67)(33,"div",68),l(34,"Tool access"),a(),o(35,"app-segmented",69),u("change",function(p){_(e);let m=s();return f(m.setToolAccess(p))}),a()()()}if(n&2){let e=s();i(5),d("size",13),i(3),d("size",13),i(7),d("size",13),i(5),h(e.api.isConfigured?"\u2013":"offline"),i(2),d("size",13),i(8),w(e.mcpTools),i(5),d("value",e.toolAccess())("options",e.toolAccessOptions)}}var B=[{cmd:"/ss-spec",arg:"REQ-ID | new <desc>",desc:"Intent door \u2014 view, author, implement, or review a spec"},{cmd:"/ss-explore",arg:"symbols | from\u2192to",desc:"Reads door \u2014 explore an area, trace a flow, or get impact"},{cmd:"/ss-check",arg:"(gate) | drifted | health",desc:"Gate & health door \u2014 run the gate, review drift, or see health"}],R=["specship_explore","specship_search","specship_spec","specship_link_verify"],K=class n{sanitizer=k(F);api=k(V);listEl=A("listEl");messages=b([{role:"assistant",model:"Opus 4",text:`Hi \u2014 ask anything about your specs, code, or recent Claude Code sessions.
2
-
3
- Try **/ss-spec REQ-AUTH-005** to inspect a requirement, or **/ss-explore validateSession** to walk its callers.`}]);draft=b("");thinking=b(!1);contextCollapsed=b(!1);enabledTools=b(new Set(R));toolAccess=b("safe");slashCommands=B;mcpTools=R;toolAccessOptions=[{value:"ask",label:"Ask"},{value:"safe",label:"Auto-safe"},{value:"all",label:"All"}];showSlash=T(()=>{let t=this.draft();return t.startsWith("/")&&!t.includes(" ")});filteredSlash=T(()=>{let t=this.draft();return B.filter(e=>e.cmd.startsWith(t))});setDraft(t){this.draft.set(t)}onKey(t){t.key==="Enter"&&!t.shiftKey&&(t.preventDefault(),this.send())}send(){let t=this.draft().trim();t&&(this.draft.set(""),this.messages.update(e=>[...e,{role:"user",text:t}]),this.thinking.set(!0),this.scrollToBottom(),setTimeout(()=>{this.thinking.set(!1);let e=t.startsWith("/ss-spec")?"specship_spec":"specship_explore",r=t.split(" ")[1]||"validateSession";this.messages.update(p=>[...p,{role:"assistant",model:"Opus 4",text:t.startsWith("/ss-spec")?"Looking that up in the graph \u2014 the spec resolves to one link. See the **state** pill and linked code below for whether it's drifted.":"Got it. I'll use the specship tools to answer that with structural context rather than re-reading files. Here's what I found in the current project's graph.",tools:[{name:e,input:r,output:"3 nodes \xB7 2 edges returned",status:"ok"}],cost:+(Math.random()*.3+.15).toFixed(2),tokens:Math.round(Math.random()*8e3+6e3)}]),this.scrollToBottom()},1100))}pickSlash(t){this.draft.set(t+" ")}toggleTool(t){this.enabledTools.update(e=>{let r=new Set(e);return r.has(t)?r.delete(t):r.add(t),r})}setToolAccess(t){this.toolAccess.set(t)}toggleContext(){this.contextCollapsed.update(t=>!t)}toggleToolCall(t,e){e.open=!e.open,this.messages.update(r=>[...r])}fmtK(t){return t>=1e3?`${(t/1e3).toFixed(1)}k`:`${t}`}renderMd(t){let e=t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/\*\*(.+?)\*\*/g,'<strong style="color:var(--text-primary)">$1</strong>').replace(/`(.+?)`/g,'<code class="mono" style="font-size:12px;background:var(--bg-canvas);padding:1px 5px;border-radius:4px;border:1px solid var(--border-subtle);color:var(--node-spec)">$1</code>').replace(/\n\n/g,"<br/><br/>").replace(/\n/g,"<br/>");return this.sanitizer.bypassSecurityTrustHtml(e)}scrollToBottom(){setTimeout(()=>{let t=this.listEl()?.nativeElement;t&&(t.scrollTop=t.scrollHeight)},0)}static \u0275fac=function(e){return new(e||n)};static \u0275cmp=z({type:n,selectors:[["app-chat"]],viewQuery:function(e,r){e&1&&P(r.listEl,W,5),e&2&&D()},decls:29,vars:8,consts:[["listEl",""],[1,"chat-page"],[1,"chat-col"],[1,"thread","scroll-y"],[1,"thread-inner"],[1,"row","gap-8","muted","thinking"],[1,"composer"],[1,"composer-inner"],["role","listbox","aria-label","Slash commands",1,"slash-menu"],[1,"composer-box","row"],["type","button","title","Attach files or spec IDs",1,"btn","btn-ghost","btn-sm","attach-btn"],["name","paperclip",3,"size"],["placeholder","Ask with specship context\u2026 / for commands, Tab to complete spec IDs","rows","1",1,"composer-ta",3,"input","keydown","value"],["type","button","aria-label","Send message",1,"btn","btn-primary","btn-sm","send-btn",3,"click","disabled"],["name","send",3,"size"],[1,"composer-footer","row","gap-8"],[1,"row","gap-6"],["name","bot",3,"size"],[1,"muted",2,"font-size","10.5px"],[1,"grow"],[1,"ctx-collapsed"],["aria-label","Context panel",1,"ctx-panel","col"],[1,"msg-user"],[1,"msg-assistant"],[1,"bubble-user",3,"innerHTML"],[1,"row","gap-8","asst-meta"],["aria-hidden","true",3,"size","glow"],[2,"font-size","12px","font-weight","600"],[1,"mono","muted",2,"font-size","10.5px"],[1,"asst-body"],[1,"bubble-asst",3,"innerHTML"],[1,"row","gap-8","muted",2,"margin-top","10px","font-size","10.5px","font-family","var(--font-mono)"],[1,"tool-call"],["type","button",1,"tool-header","row","gap-8",3,"click"],[2,"color","var(--text-muted)",3,"name","size"],["name","wrench",2,"color","var(--node-code)",3,"size"],[1,"mono","grow",2,"font-size","11.5px"],[1,"muted"],[1,"pill",2,"font-size","9.5px","color","var(--success)","background","var(--success-soft)"],["name","check",3,"size"],[1,"tool-output"],[1,"mono","muted",2,"font-size","10.5px","margin-bottom","3px"],[1,"mono",2,"font-size","11.5px","color","var(--text-secondary)"],["name","coins",3,"size"],[1,"thinking-dot"],["type","button","role","option",1,"slash-item","row","gap-8"],["type","button","role","option",1,"slash-item","row","gap-8",3,"click"],[1,"mono",2,"font-size","12px","color","var(--accent)"],[1,"mono","muted",2,"font-size","11px"],[1,"muted","grow",2,"font-size","11px","text-align","right"],["type","button","title","Show context",1,"btn","btn-ghost","btn-xs",3,"click"],["name","chevronLeft",3,"size"],[1,"ctx-header","row"],[1,"eyebrow","grow"],["type","button",1,"btn","btn-ghost","btn-xs",3,"click"],["name","chevronRight",3,"size"],[1,"ctx-body","scroll-y"],[1,"ctx-row","row","gap-8"],["name","folder",2,"color","var(--text-muted)","flex-shrink","0",3,"size"],[2,"min-width","0"],[1,"muted",2,"font-size","10px"],[1,"mono",2,"font-size","11.5px","overflow","hidden","text-overflow","ellipsis","white-space","nowrap"],["name","box",2,"color","var(--text-muted)","flex-shrink","0",3,"size"],[1,"mono",2,"font-size","11.5px"],["name","drift",2,"color","var(--warn)","flex-shrink","0",3,"size"],[1,"eyebrow",2,"margin","16px 0 8px"],[1,"row","gap-8",2,"padding","5px 0"],[1,"ctx-footer"],[1,"eyebrow",2,"margin-bottom","6px"],["size","sm",3,"change","value","options"],["type","button",1,"toggle-btn",3,"click"],[1,"toggle-thumb"],[1,"mono",2,"font-size","11px"]],template:function(e,r){e&1&&(o(0,"div",1)(1,"div",2)(2,"div",3,0)(4,"div",4),y(5,ne,2,1,null,null,E),x(7,ie,3,0,"div",5),a()(),o(8,"div",6)(9,"div",7),x(10,re,3,0,"div",8),o(11,"div",9)(12,"button",10),c(13,"app-icon",11),a(),o(14,"textarea",12),u("input",function(m){return r.setDraft(m.target.value)})("keydown",function(m){return r.onKey(m)}),a(),o(15,"button",13),u("click",function(){return r.send()}),c(16,"app-icon",14),a()(),o(17,"div",15)(18,"div",16)(19,"app-pill"),c(20,"app-icon",17),l(21," Opus 4 "),a(),o(22,"span",18),l(23,"single thread \xB7 this project"),a()(),c(24,"span",19),o(25,"span",18),l(26,"Enter to send \xB7 Shift+Enter for newline"),a()()()()(),x(27,le,3,1,"div",20)(28,de,36,7,"aside",21),a()),e&2&&(i(5),w(r.messages()),i(2),g(r.thinking()?7:-1),i(3),g(r.showSlash()&&r.filteredSlash().length>0?10:-1),i(3),d("size",15),i(),d("value",r.draft()),i(),d("disabled",!r.draft().trim()),i(),d("size",14),i(4),d("size",10),i(7),g(r.contextCollapsed()?27:28))},dependencies:[H,L,N,j],styles:["[_nghost-%COMP%]{display:flex;flex:1;min-height:0}.chat-page[_ngcontent-%COMP%]{display:flex;flex:1;min-height:0;min-width:0}.chat-col[_ngcontent-%COMP%]{display:flex;flex-direction:column;flex:1;min-width:0;min-height:0}.thread[_ngcontent-%COMP%]{flex:1;padding:20px 24px;overflow-y:auto}.thread-inner[_ngcontent-%COMP%]{max-width:760px;margin:0 auto}.msg-user[_ngcontent-%COMP%]{display:flex;justify-content:flex-end;margin-bottom:18px}.bubble-user[_ngcontent-%COMP%]{max-width:76%;background:var(--bg-elevated);border:1px solid var(--border-subtle);border-radius:12px 12px 4px;padding:9px 13px;font-size:13.5px;line-height:1.55}.msg-assistant[_ngcontent-%COMP%]{margin-bottom:22px}.asst-meta[_ngcontent-%COMP%]{align-items:center;margin-bottom:8px}.asst-logo[_ngcontent-%COMP%]{display:inline-grid;place-items:center;width:22px;height:22px;border-radius:50%;background:var(--accent);color:#fff;font-size:11px;font-weight:700;flex-shrink:0}.asst-body[_ngcontent-%COMP%]{padding-left:30px}.bubble-asst[_ngcontent-%COMP%]{font-size:13.5px;line-height:1.62;color:var(--text-secondary);text-wrap:pretty;margin-bottom:4px}.tool-call[_ngcontent-%COMP%]{border:1px solid var(--border-subtle);border-radius:7px;margin-top:8px;overflow:hidden;background:var(--bg-canvas)}.tool-header[_ngcontent-%COMP%]{width:100%;background:transparent;border:none;cursor:pointer;padding:6px 10px;text-align:left;color:inherit;font-family:var(--font-ui)}.tool-header[_ngcontent-%COMP%]:hover{background:var(--bg-hover)}.tool-header[_ngcontent-%COMP%]:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.tool-output[_ngcontent-%COMP%]{padding:8px 10px 10px 30px;border-top:1px solid var(--border-subtle)}.thinking[_ngcontent-%COMP%]{padding-left:30px;font-size:12px}.thinking-dot[_ngcontent-%COMP%]{width:6px;height:6px;border-radius:50%;background:var(--accent);animation:skeleton 1s infinite;flex-shrink:0}.composer[_ngcontent-%COMP%]{padding:12px 24px 18px;border-top:1px solid var(--border-subtle);flex-shrink:0}.composer-inner[_ngcontent-%COMP%]{max-width:760px;margin:0 auto;position:relative}.composer-box[_ngcontent-%COMP%]{align-items:flex-end;background:var(--bg-panel);border:1px solid var(--border-subtle);border-radius:12px;padding:8px;gap:8px}.attach-btn[_ngcontent-%COMP%]{padding:7px;flex-shrink:0}.send-btn[_ngcontent-%COMP%]{padding:7px 10px;flex-shrink:0}.composer-ta[_ngcontent-%COMP%]{flex:1;resize:none;background:transparent;border:none;outline:none;color:var(--text-primary);font-size:13.5px;font-family:var(--font-ui);line-height:1.5;padding:5px 2px;max-height:120px}.composer-ta[_ngcontent-%COMP%]::placeholder{color:var(--text-muted)}.composer-footer[_ngcontent-%COMP%]{margin-top:8px;align-items:center}.slash-menu[_ngcontent-%COMP%]{position:absolute;bottom:100%;left:0;margin-bottom:8px;background:var(--bg-elevated);border:1px solid var(--border-strong);border-radius:8px;box-shadow:var(--shadow-pop);padding:5px;width:320px;z-index:10}.slash-item[_ngcontent-%COMP%]{width:100%;background:transparent;border:none;cursor:pointer;padding:6px 9px;border-radius:5px;text-align:left;color:inherit;font-family:var(--font-ui)}.slash-item[_ngcontent-%COMP%]:hover{background:var(--bg-hover)}.slash-item[_ngcontent-%COMP%]:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.ctx-collapsed[_ngcontent-%COMP%]{width:40px;flex-shrink:0;border-left:1px solid var(--border-subtle);display:flex;justify-content:center;padding-top:12px}.ctx-panel[_ngcontent-%COMP%]{width:264px;flex-shrink:0;border-left:1px solid var(--border-subtle);background:var(--bg-panel);display:flex;flex-direction:column;min-height:0}.ctx-header[_ngcontent-%COMP%]{padding:11px 13px;border-bottom:1px solid var(--border-subtle);align-items:center}.ctx-body[_ngcontent-%COMP%]{flex:1;padding:13px;overflow-y:auto;min-height:0}.ctx-row[_ngcontent-%COMP%]{padding:5px 0;align-items:flex-start}.ctx-footer[_ngcontent-%COMP%]{padding:12px;border-top:1px solid var(--border-subtle)}.toggle-btn[_ngcontent-%COMP%]{width:30px;height:17px;border-radius:999px;border:none;cursor:pointer;padding:2px;background:var(--bg-elevated);display:flex;justify-content:flex-start;align-items:center;transition:background .12s,justify-content .12s;flex-shrink:0}.toggle-btn.on[_ngcontent-%COMP%]{background:var(--accent);justify-content:flex-end}.toggle-btn[_ngcontent-%COMP%]:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.toggle-thumb[_ngcontent-%COMP%]{width:13px;height:13px;border-radius:50%;background:#fff;flex-shrink:0}.eyebrow[_ngcontent-%COMP%]{color:var(--text-muted);font-size:10.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}.muted[_ngcontent-%COMP%]{color:var(--text-muted)}.mono[_ngcontent-%COMP%]{font-family:var(--font-mono)}.pill[_ngcontent-%COMP%]{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;padding:3px 9px;border-radius:999px;line-height:1.4;white-space:nowrap}.grow[_ngcontent-%COMP%]{flex:1}"],changeDetection:0})};export{K as Chat};