@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.
- package/lib/dist/server/chat/classify.js +110 -0
- package/lib/dist/server/routes/chat-answer.js +488 -0
- package/lib/dist/server/routes/chat.js +145 -0
- package/lib/dist/server/server.js +2 -0
- package/lib/dist/web/chunk-6FZTSHJS.js +3 -0
- package/lib/dist/web/index.html +1 -1
- package/lib/dist/web/{main-FHGJXJSF.js → main-A6QZ3DLK.js} +1 -1
- package/lib/node_modules/.package-lock.json +1 -1
- package/lib/package.json +1 -1
- package/package.json +1 -1
- package/lib/dist/web/chunk-VPUORMLB.js +0 -3
|
@@ -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,"&").replace(/</g,"<").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};
|
package/lib/dist/web/index.html
CHANGED
|
@@ -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-
|
|
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)})});
|
package/lib/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@specship/specship",
|
|
3
|
-
"version": "0.11.
|
|
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,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,"&").replace(/</g,"<").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};
|