1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
|
import type { LspServerInfo, LspServerState } from "@dispatch/transport-contract";
/**
* Pure core for the workspace feature — zero DOM, zero effects, zero Svelte.
*
* The workspace feature exposes a conversation's per-tab working directory (cwd)
* and the live status of the language servers configured for that cwd. This
* module holds the pure logic: cwd normalization/validation, the mapping of a
* backend `LspServerState` to a display badge, and a one-line server summary.
* The effects (the HTTP get/set cwd + get LSP status) are INJECTED via the ports
* below; the composition root implements them.
*/
// ── Injected ports (consumer-defines-port; the composition root adapts the
// store's HTTP calls to these shapes). ──────────────────────────────────────
/** Outcome of `PUT /conversations/:id/cwd`; `null` when no real conversation is focused. */
export type CwdSaveResult =
| { readonly ok: true; readonly cwd: string | null }
| { readonly ok: false; readonly error: string };
export type SaveCwd = (cwd: string) => Promise<CwdSaveResult | null>;
/** Outcome of `GET /conversations/:id/lsp`; `null` when no real conversation is focused. */
export type LspStatusResult =
| { readonly ok: true; readonly cwd: string | null; readonly servers: readonly LspServerInfo[] }
| { readonly ok: false; readonly error: string };
export type LoadLspStatus = () => Promise<LspStatusResult | null>;
// ── cwd helpers ───────────────────────────────────────────────────────────────
/** Trim surrounding whitespace; the backend rejects an empty cwd. */
export function normalizeCwd(raw: string): string {
return raw.trim();
}
/** Whether a typed cwd is submittable (non-empty after trim). */
export function isSubmittableCwd(raw: string): boolean {
return normalizeCwd(raw).length > 0;
}
/**
* Whether saving `typed` would change the persisted `current` cwd. A no-op save
* (unchanged, or empty) should be disabled.
*/
export function cwdChanged(typed: string, current: string | null): boolean {
const next = normalizeCwd(typed);
if (next.length === 0) return false;
return next !== (current ?? "");
}
// ── LSP server status → display view ──────────────────────────────────────────
export type Badge = "success" | "warning" | "error" | "neutral";
export interface LspServerView {
readonly id: string;
readonly name: string;
readonly root: string;
/** Space-joined extension list, e.g. ".ts .tsx". */
readonly extensionsLabel: string;
readonly state: LspServerState;
readonly statusLabel: string;
readonly badge: Badge;
/** True while the state is transient (show a spinner). */
readonly busy: boolean;
/** The error reason when `state === "error"`, else null. */
readonly error: string | null;
}
/** Map a server's state to a display label + badge severity + busy flag. */
export function viewLspServer(server: LspServerInfo): LspServerView {
let statusLabel: string;
let badge: Badge;
let busy = false;
switch (server.state) {
case "connected":
statusLabel = "Connected";
badge = "success";
break;
case "starting":
statusLabel = "Starting…";
badge = "warning";
busy = true;
break;
case "not-started":
statusLabel = "Not started";
badge = "neutral";
busy = true;
break;
case "error":
statusLabel = "Error";
badge = "error";
break;
}
return {
id: server.id,
name: server.name,
root: server.root,
extensionsLabel: server.extensions.join(" "),
state: server.state,
statusLabel,
badge,
busy,
error: server.state === "error" ? (server.error ?? "Failed to start") : null,
};
}
export function viewLspServers(servers: readonly LspServerInfo[]): readonly LspServerView[] {
return servers.map(viewLspServer);
}
/** A short one-line summary, e.g. "2 connected" / "1 connected, 1 error". */
export function summarizeServers(servers: readonly LspServerInfo[]): string {
if (servers.length === 0) return "No language servers";
let connected = 0;
let errored = 0;
let pending = 0;
for (const s of servers) {
if (s.state === "connected") connected++;
else if (s.state === "error") errored++;
else pending++;
}
const parts: string[] = [];
if (connected > 0) parts.push(`${connected} connected`);
if (pending > 0) parts.push(`${pending} starting`);
if (errored > 0) parts.push(`${errored} error${errored === 1 ? "" : "s"}`);
return parts.join(", ");
}
|