@nextclaw/companion 0.1.1-beta.0 → 0.1.1-beta.2
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/CHANGELOG.md +49 -0
- package/dist/{src/launcher/index.js → electron/launcher.js} +2 -2
- package/dist/{src → electron}/main.js +3 -3
- package/dist/{src/preload/index.js → electron/preload.js} +1 -6
- package/dist/{src → electron}/services/companion-application.service.js +14 -16
- package/dist/{src/stores/companion-runtime-state.store.js → electron/services/companion-runtime-state-persistence.service.js} +3 -3
- package/dist/{src/stores/companion-window-position.store.js → electron/services/companion-window-position-persistence.service.js} +4 -4
- package/dist/{src → electron}/services/companion-window.service.js +15 -26
- package/dist/electron/types/companion-shell.types.js +2 -0
- package/dist/src/app/app.js +17 -0
- package/dist/src/app/components/companion-shell.container.js +20 -0
- package/dist/src/app/main.js +10 -0
- package/dist/src/app/providers/companion-presenter.provider.js +17 -0
- package/dist/src/managers/companion-runtime.manager.js +152 -0
- package/dist/src/managers/companion-runtime.manager.test.js +45 -0
- package/dist/src/managers/companion-shell.manager.js +22 -0
- package/dist/src/presenters/companion.presenter.js +37 -0
- package/dist/src/services/companion-runtime-client.service.js +16 -68
- package/dist/src/stores/companion-runtime.store.js +27 -0
- package/dist/src/stores/companion-shell.store.js +18 -0
- package/dist/ui/assets/dist-D4AJsX_N.js +1 -0
- package/dist/ui/assets/index-BHBNwOpX.js +8 -0
- package/dist/ui/assets/index-DPctoCcE.css +1 -0
- package/dist/ui/index.html +13 -0
- package/{src/launcher/index.ts → electron/launcher.ts} +2 -2
- package/{src → electron}/main.ts +4 -3
- package/electron/preload.ts +7 -0
- package/{src → electron}/services/companion-application.service.ts +16 -20
- package/{src/stores/companion-runtime-state.store.ts → electron/services/companion-runtime-state-persistence.service.ts} +1 -1
- package/{src/stores/companion-window-position.store.ts → electron/services/companion-window-position-persistence.service.ts} +3 -3
- package/{src → electron}/services/companion-window.service.ts +23 -30
- package/electron/types/companion-shell.types.ts +8 -0
- package/index.html +12 -0
- package/module-structure.config.json +25 -0
- package/package.json +21 -9
- package/postcss.config.mjs +6 -0
- package/scripts/smoke.mjs +4 -3
- package/src/app/app.tsx +17 -0
- package/src/app/components/companion-shell.container.tsx +67 -0
- package/src/app/index.css +37 -0
- package/src/app/main.tsx +15 -0
- package/src/app/providers/companion-presenter.provider.tsx +27 -0
- package/src/managers/companion-runtime.manager.test.ts +56 -0
- package/src/managers/companion-runtime.manager.ts +191 -0
- package/src/managers/companion-shell.manager.ts +21 -0
- package/src/module-structure.config.json +22 -0
- package/src/presenters/companion.presenter.ts +36 -0
- package/src/services/companion-runtime-client.service.ts +21 -84
- package/src/stores/companion-runtime.store.ts +51 -0
- package/src/stores/companion-shell.store.ts +27 -0
- package/src/types/companion-bridge.types.d.ts +11 -0
- package/src/types/companion.types.ts +0 -10
- package/tailwind.config.mjs +32 -0
- package/tsconfig.json +2 -1
- package/vite.config.mts +16 -0
- package/dist/src/companion-session-view.service.test.js +0 -27
- package/dist/src/services/companion-session-view.service.js +0 -52
- package/dist/src/utils/companion-renderer-html.utils.js +0 -194
- package/src/companion-session-view.service.test.ts +0 -37
- package/src/preload/index.ts +0 -13
- package/src/services/companion-session-view.service.ts +0 -63
- package/src/utils/companion-renderer-html.utils.ts +0 -192
- /package/dist/{src → electron}/services/companion-tray.service.js +0 -0
- /package/{src → electron}/services/companion-tray.service.ts +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { CompanionRuntimeClientService } from "../services/companion-runtime-client.service.js";
|
|
2
|
+
import {
|
|
3
|
+
useCompanionRuntimeStore,
|
|
4
|
+
createInitialCompanionRuntimeSnapshot,
|
|
5
|
+
type CompanionRuntimeConnectionState,
|
|
6
|
+
type CompanionRuntimeSnapshot
|
|
7
|
+
} from "../stores/companion-runtime.store.js";
|
|
8
|
+
import type {
|
|
9
|
+
CompanionAgentProfile,
|
|
10
|
+
CompanionAvatarView,
|
|
11
|
+
CompanionOfflineReason,
|
|
12
|
+
CompanionSessionSummary
|
|
13
|
+
} from "../types/companion.types.js";
|
|
14
|
+
|
|
15
|
+
export class CompanionRuntimeManager {
|
|
16
|
+
private clientService: CompanionRuntimeClientService | null = null;
|
|
17
|
+
private baseUrl: string | null = null;
|
|
18
|
+
private refreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
19
|
+
private subscription: { close: () => void } | null = null;
|
|
20
|
+
private started = false;
|
|
21
|
+
|
|
22
|
+
readonly start = async (baseUrl: string): Promise<void> => {
|
|
23
|
+
if (this.started && this.baseUrl === baseUrl) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
this.stop();
|
|
27
|
+
this.baseUrl = baseUrl;
|
|
28
|
+
this.clientService = new CompanionRuntimeClientService(baseUrl);
|
|
29
|
+
await this.clientService.initialize();
|
|
30
|
+
useCompanionRuntimeStore.getState().setSnapshot(createInitialCompanionRuntimeSnapshot(baseUrl));
|
|
31
|
+
await this.refresh();
|
|
32
|
+
this.subscription = this.ensureClientService().subscribeToSessions(
|
|
33
|
+
async () => {
|
|
34
|
+
await this.refresh();
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
reconnectDelayMs: 1000,
|
|
38
|
+
onError: async () => {
|
|
39
|
+
await this.refresh();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
this.refreshTimer = setInterval(() => {
|
|
44
|
+
void this.refresh();
|
|
45
|
+
}, 10000);
|
|
46
|
+
this.started = true;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
readonly stop = (): void => {
|
|
50
|
+
this.subscription?.close();
|
|
51
|
+
this.subscription = null;
|
|
52
|
+
if (this.refreshTimer !== null) {
|
|
53
|
+
clearInterval(this.refreshTimer);
|
|
54
|
+
this.refreshTimer = null;
|
|
55
|
+
}
|
|
56
|
+
this.clientService = null;
|
|
57
|
+
this.baseUrl = null;
|
|
58
|
+
this.started = false;
|
|
59
|
+
useCompanionRuntimeStore.getState().reset();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
readonly getSnapshot = (): CompanionRuntimeSnapshot => {
|
|
63
|
+
return useCompanionRuntimeStore.getState().snapshot;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
readonly syncRuntimeSnapshot = ({
|
|
67
|
+
agents,
|
|
68
|
+
sessions
|
|
69
|
+
}: {
|
|
70
|
+
agents: CompanionAgentProfile[];
|
|
71
|
+
sessions: CompanionSessionSummary[];
|
|
72
|
+
}): CompanionRuntimeSnapshot => {
|
|
73
|
+
const baseUrl = this.ensureBaseUrl();
|
|
74
|
+
const currentView = this.resolveCurrentView({
|
|
75
|
+
baseUrl,
|
|
76
|
+
agents,
|
|
77
|
+
sessions
|
|
78
|
+
});
|
|
79
|
+
const connectionState: CompanionRuntimeConnectionState = currentView.state === "running" ? "running" : "idle";
|
|
80
|
+
const snapshot: CompanionRuntimeSnapshot = {
|
|
81
|
+
baseUrl,
|
|
82
|
+
agents,
|
|
83
|
+
sessions,
|
|
84
|
+
connectionState,
|
|
85
|
+
offlineReason: null,
|
|
86
|
+
currentView
|
|
87
|
+
};
|
|
88
|
+
useCompanionRuntimeStore.getState().setSnapshot(snapshot);
|
|
89
|
+
return snapshot;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
readonly applyOfflineState = (reason?: CompanionOfflineReason): CompanionRuntimeSnapshot => {
|
|
93
|
+
const baseUrl = this.baseUrl ?? useCompanionRuntimeStore.getState().snapshot.baseUrl ?? "http://127.0.0.1:55667";
|
|
94
|
+
const snapshot: CompanionRuntimeSnapshot = {
|
|
95
|
+
baseUrl,
|
|
96
|
+
agents: [],
|
|
97
|
+
sessions: [],
|
|
98
|
+
connectionState: "offline",
|
|
99
|
+
offlineReason: reason ?? null,
|
|
100
|
+
currentView: {
|
|
101
|
+
state: "offline",
|
|
102
|
+
title: "NextClaw",
|
|
103
|
+
subtitle: reason?.summary?.trim() || "Runtime unavailable",
|
|
104
|
+
openUrl: baseUrl
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
useCompanionRuntimeStore.getState().setSnapshot(snapshot);
|
|
108
|
+
return snapshot;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
private readonly refresh = async (): Promise<void> => {
|
|
112
|
+
try {
|
|
113
|
+
const [agents, sessions] = await Promise.all([
|
|
114
|
+
this.ensureClientService().listAgents(),
|
|
115
|
+
this.ensureClientService().listSessions()
|
|
116
|
+
]);
|
|
117
|
+
this.syncRuntimeSnapshot({
|
|
118
|
+
agents,
|
|
119
|
+
sessions: sessions.sessions
|
|
120
|
+
});
|
|
121
|
+
} catch (error) {
|
|
122
|
+
this.applyOfflineState(
|
|
123
|
+
error instanceof Error
|
|
124
|
+
? { summary: this.summarizeOfflineError(error.message) }
|
|
125
|
+
: undefined
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
private readonly ensureClientService = (): CompanionRuntimeClientService => {
|
|
131
|
+
if (!this.clientService) {
|
|
132
|
+
this.clientService = new CompanionRuntimeClientService(this.ensureBaseUrl());
|
|
133
|
+
}
|
|
134
|
+
return this.clientService;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
private readonly ensureBaseUrl = (): string => {
|
|
138
|
+
if (!this.baseUrl) {
|
|
139
|
+
throw new Error("CompanionRuntimeManager baseUrl has not been initialized.");
|
|
140
|
+
}
|
|
141
|
+
return this.baseUrl;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
private readonly resolveCurrentView = ({
|
|
145
|
+
baseUrl,
|
|
146
|
+
agents,
|
|
147
|
+
sessions
|
|
148
|
+
}: {
|
|
149
|
+
baseUrl: string;
|
|
150
|
+
agents: CompanionAgentProfile[];
|
|
151
|
+
sessions: CompanionSessionSummary[];
|
|
152
|
+
}): CompanionAvatarView => {
|
|
153
|
+
const runningSession = [...sessions]
|
|
154
|
+
.filter((session) => session.status === "running")
|
|
155
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0];
|
|
156
|
+
|
|
157
|
+
if (!runningSession) {
|
|
158
|
+
return {
|
|
159
|
+
state: "idle",
|
|
160
|
+
title: "NextClaw",
|
|
161
|
+
subtitle: "No active agent",
|
|
162
|
+
openUrl: baseUrl
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const agent = agents.find((entry) => entry.id === runningSession.agentId) ?? null;
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
state: "running",
|
|
170
|
+
title: agent?.displayName?.trim() || runningSession.agentId || "Active Agent",
|
|
171
|
+
subtitle: runningSession.sessionId,
|
|
172
|
+
avatarUrl: runningSession.agentId
|
|
173
|
+
? agent?.avatarUrl?.trim() || this.ensureClientService().resolveAvatarUrl(runningSession.agentId)
|
|
174
|
+
: undefined,
|
|
175
|
+
sessionId: runningSession.sessionId,
|
|
176
|
+
agentId: runningSession.agentId,
|
|
177
|
+
openUrl: baseUrl
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
private readonly summarizeOfflineError = (message: string): string => {
|
|
182
|
+
if (/fetch failed/i.test(message)) {
|
|
183
|
+
return "Cannot reach runtime";
|
|
184
|
+
}
|
|
185
|
+
if (/timed out/i.test(message)) {
|
|
186
|
+
return "Runtime timeout";
|
|
187
|
+
}
|
|
188
|
+
const trimmed = message.trim();
|
|
189
|
+
return trimmed.length > 28 ? `${trimmed.slice(0, 25)}...` : trimmed || "Runtime unavailable";
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useCompanionShellStore, type CompanionShellSnapshot } from "../stores/companion-shell.store.js";
|
|
2
|
+
|
|
3
|
+
export class CompanionShellManager {
|
|
4
|
+
readonly bootstrap = async (): Promise<CompanionShellSnapshot> => {
|
|
5
|
+
const bootstrap = await window.nextclawCompanion.getBootstrap();
|
|
6
|
+
const snapshot: CompanionShellSnapshot = {
|
|
7
|
+
baseUrl: bootstrap.baseUrl,
|
|
8
|
+
bootstrapped: true
|
|
9
|
+
};
|
|
10
|
+
useCompanionShellStore.getState().setSnapshot(snapshot);
|
|
11
|
+
return snapshot;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
readonly open = async (): Promise<void> => {
|
|
15
|
+
await window.nextclawCompanion.open();
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
readonly quit = async (): Promise<void> => {
|
|
19
|
+
await window.nextclawCompanion.quit();
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"contractKind": "legacy",
|
|
3
|
+
"organizationModel": "legacy-apps-companion-renderer-root",
|
|
4
|
+
"rootPolicy": "contract-only",
|
|
5
|
+
"enforcement": "error",
|
|
6
|
+
"allowedRootDirectories": [
|
|
7
|
+
"app",
|
|
8
|
+
"managers",
|
|
9
|
+
"presenters",
|
|
10
|
+
"services",
|
|
11
|
+
"stores",
|
|
12
|
+
"types"
|
|
13
|
+
],
|
|
14
|
+
"requiredRootDirectories": [
|
|
15
|
+
"presenters",
|
|
16
|
+
"managers",
|
|
17
|
+
"stores"
|
|
18
|
+
],
|
|
19
|
+
"allowedRootFiles": [
|
|
20
|
+
"module-structure.config.json"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { CompanionRuntimeManager } from "../managers/companion-runtime.manager.js";
|
|
2
|
+
import { CompanionShellManager } from "../managers/companion-shell.manager.js";
|
|
3
|
+
|
|
4
|
+
export class CompanionPresenter {
|
|
5
|
+
readonly companionRuntimeManager = new CompanionRuntimeManager();
|
|
6
|
+
readonly companionShellManager = new CompanionShellManager();
|
|
7
|
+
private bootstrapPromise: Promise<void> | null = null;
|
|
8
|
+
private bootstrapped = false;
|
|
9
|
+
|
|
10
|
+
readonly bootstrap = async (): Promise<void> => {
|
|
11
|
+
if (this.bootstrapped) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (this.bootstrapPromise) {
|
|
15
|
+
return await this.bootstrapPromise;
|
|
16
|
+
}
|
|
17
|
+
this.bootstrapPromise = (async () => {
|
|
18
|
+
const shellSnapshot = await this.companionShellManager.bootstrap();
|
|
19
|
+
await this.companionRuntimeManager.start(shellSnapshot.baseUrl);
|
|
20
|
+
this.bootstrapped = true;
|
|
21
|
+
})();
|
|
22
|
+
try {
|
|
23
|
+
await this.bootstrapPromise;
|
|
24
|
+
} finally {
|
|
25
|
+
this.bootstrapPromise = null;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
readonly shutdown = (): void => {
|
|
30
|
+
this.companionRuntimeManager.stop();
|
|
31
|
+
this.bootstrapped = false;
|
|
32
|
+
this.bootstrapPromise = null;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const companionPresenter = new CompanionPresenter();
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
CompanionAgentProfile,
|
|
3
|
-
CompanionAvatarView,
|
|
4
|
-
CompanionSessionSummary
|
|
5
|
-
} from "../types/companion.types.js";
|
|
6
|
-
import { CompanionSessionViewService } from "./companion-session-view.service.js";
|
|
1
|
+
import type { CompanionAgentProfile, CompanionSessionSummary } from "../types/companion.types.js";
|
|
7
2
|
|
|
8
3
|
type CompanionSdkClient = {
|
|
9
4
|
agents: {
|
|
@@ -21,98 +16,40 @@ type CompanionSdkClient = {
|
|
|
21
16
|
|
|
22
17
|
export class CompanionRuntimeClientService {
|
|
23
18
|
private client: CompanionSdkClient | null = null;
|
|
24
|
-
private viewService: CompanionSessionViewService | null = null;
|
|
25
|
-
private refreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
26
|
-
private subscription: { close: () => void } | null = null;
|
|
27
19
|
|
|
28
20
|
constructor(private readonly baseUrl: string) {}
|
|
29
21
|
|
|
30
|
-
readonly
|
|
31
|
-
await this.ensureClient();
|
|
32
|
-
await this.refresh(onView);
|
|
33
|
-
const client = await this.ensureClient();
|
|
34
|
-
this.subscription = client.sessions.subscribe(
|
|
35
|
-
async () => {
|
|
36
|
-
await this.refresh(onView);
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
reconnectDelayMs: 1000,
|
|
40
|
-
onError: async () => {
|
|
41
|
-
await this.refresh(onView);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
);
|
|
45
|
-
this.refreshTimer = setInterval(() => {
|
|
46
|
-
void this.refresh(onView);
|
|
47
|
-
}, 10000);
|
|
22
|
+
readonly listAgents = async (): Promise<CompanionAgentProfile[]> => {
|
|
23
|
+
return await this.ensureClient().agents.list();
|
|
48
24
|
};
|
|
49
25
|
|
|
50
|
-
readonly
|
|
51
|
-
this.
|
|
52
|
-
this.subscription = null;
|
|
53
|
-
if (this.refreshTimer !== null) {
|
|
54
|
-
clearInterval(this.refreshTimer);
|
|
55
|
-
this.refreshTimer = null;
|
|
56
|
-
}
|
|
26
|
+
readonly listSessions = async (): Promise<{ sessions: CompanionSessionSummary[] }> => {
|
|
27
|
+
return await this.ensureClient().sessions.list();
|
|
57
28
|
};
|
|
58
29
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
client.sessions.list()
|
|
65
|
-
]);
|
|
66
|
-
onView(
|
|
67
|
-
this.ensureViewService(client).selectView({
|
|
68
|
-
agents,
|
|
69
|
-
sessions: sessions.sessions
|
|
70
|
-
})
|
|
71
|
-
);
|
|
72
|
-
} catch (error) {
|
|
73
|
-
console.error("[companion] refresh failed", error);
|
|
74
|
-
onView(
|
|
75
|
-
this.createOfflineView(
|
|
76
|
-
error instanceof Error
|
|
77
|
-
? { summary: this.summarizeOfflineError(error.message) }
|
|
78
|
-
: undefined
|
|
79
|
-
)
|
|
80
|
-
);
|
|
81
|
-
}
|
|
30
|
+
readonly subscribeToSessions = (
|
|
31
|
+
handler: (event: unknown) => void,
|
|
32
|
+
options: { reconnectDelayMs?: number; onError?: (error: unknown) => void }
|
|
33
|
+
): { close: () => void } => {
|
|
34
|
+
return this.ensureClient().sessions.subscribe(handler, options);
|
|
82
35
|
};
|
|
83
36
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return this.client;
|
|
87
|
-
}
|
|
88
|
-
const sdkModule = await import("@nextclaw/client-sdk");
|
|
89
|
-
this.client = sdkModule.createNextClawClient({ baseUrl: this.baseUrl }) as CompanionSdkClient;
|
|
90
|
-
return this.client;
|
|
37
|
+
readonly resolveAvatarUrl = (agentId: string): string => {
|
|
38
|
+
return `${this.baseUrl}/api/agents/${encodeURIComponent(agentId)}/avatar`;
|
|
91
39
|
};
|
|
92
40
|
|
|
93
|
-
private readonly
|
|
94
|
-
if (this.
|
|
95
|
-
return this.
|
|
41
|
+
private readonly ensureClient = (): CompanionSdkClient => {
|
|
42
|
+
if (this.client) {
|
|
43
|
+
return this.client;
|
|
96
44
|
}
|
|
97
|
-
|
|
98
|
-
return this.viewService;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
private readonly createOfflineView = (reason?: { summary: string }): CompanionAvatarView => {
|
|
102
|
-
const viewService =
|
|
103
|
-
this.viewService ??
|
|
104
|
-
new CompanionSessionViewService(this.baseUrl, (agentId) => `${this.baseUrl}/api/agents/${encodeURIComponent(agentId)}/avatar`);
|
|
105
|
-
return viewService.createOfflineView(reason);
|
|
45
|
+
throw new Error("Companion runtime client has not been initialized.");
|
|
106
46
|
};
|
|
107
47
|
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
return
|
|
111
|
-
}
|
|
112
|
-
if (/timed out/i.test(message)) {
|
|
113
|
-
return "Runtime timeout";
|
|
48
|
+
readonly initialize = async (): Promise<void> => {
|
|
49
|
+
if (this.client) {
|
|
50
|
+
return;
|
|
114
51
|
}
|
|
115
|
-
const
|
|
116
|
-
|
|
52
|
+
const sdkModule = await import("@nextclaw/client-sdk");
|
|
53
|
+
this.client = sdkModule.createNextClawClient({ baseUrl: this.baseUrl }) as CompanionSdkClient;
|
|
117
54
|
};
|
|
118
55
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
CompanionAgentProfile,
|
|
5
|
+
CompanionAvatarView,
|
|
6
|
+
CompanionOfflineReason,
|
|
7
|
+
CompanionSessionSummary
|
|
8
|
+
} from "../types/companion.types.js";
|
|
9
|
+
|
|
10
|
+
export type CompanionRuntimeConnectionState = "loading" | "idle" | "running" | "offline";
|
|
11
|
+
|
|
12
|
+
export type CompanionRuntimeSnapshot = {
|
|
13
|
+
baseUrl: string | null;
|
|
14
|
+
agents: CompanionAgentProfile[];
|
|
15
|
+
sessions: CompanionSessionSummary[];
|
|
16
|
+
connectionState: CompanionRuntimeConnectionState;
|
|
17
|
+
offlineReason: CompanionOfflineReason | null;
|
|
18
|
+
currentView: CompanionAvatarView;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type CompanionRuntimeStoreState = {
|
|
22
|
+
snapshot: CompanionRuntimeSnapshot;
|
|
23
|
+
setSnapshot: (snapshot: CompanionRuntimeSnapshot) => void;
|
|
24
|
+
reset: () => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const createInitialCompanionRuntimeSnapshot = (
|
|
28
|
+
baseUrl: string | null = null
|
|
29
|
+
): CompanionRuntimeSnapshot => ({
|
|
30
|
+
baseUrl,
|
|
31
|
+
agents: [],
|
|
32
|
+
sessions: [],
|
|
33
|
+
connectionState: "loading",
|
|
34
|
+
offlineReason: null,
|
|
35
|
+
currentView: {
|
|
36
|
+
state: "idle",
|
|
37
|
+
title: "NextClaw",
|
|
38
|
+
subtitle: "Starting companion",
|
|
39
|
+
openUrl: baseUrl ?? "http://127.0.0.1:55667"
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const useCompanionRuntimeStore = create<CompanionRuntimeStoreState>((set) => ({
|
|
44
|
+
snapshot: createInitialCompanionRuntimeSnapshot(),
|
|
45
|
+
setSnapshot: (snapshot) => {
|
|
46
|
+
set({ snapshot });
|
|
47
|
+
},
|
|
48
|
+
reset: () => {
|
|
49
|
+
set({ snapshot: createInitialCompanionRuntimeSnapshot() });
|
|
50
|
+
}
|
|
51
|
+
}));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
|
|
3
|
+
export type CompanionShellSnapshot = {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
bootstrapped: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type CompanionShellStoreState = {
|
|
9
|
+
snapshot: CompanionShellSnapshot;
|
|
10
|
+
setSnapshot: (snapshot: CompanionShellSnapshot) => void;
|
|
11
|
+
reset: () => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const createInitialCompanionShellSnapshot = (): CompanionShellSnapshot => ({
|
|
15
|
+
baseUrl: "http://127.0.0.1:55667",
|
|
16
|
+
bootstrapped: false
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const useCompanionShellStore = create<CompanionShellStoreState>((set) => ({
|
|
20
|
+
snapshot: createInitialCompanionShellSnapshot(),
|
|
21
|
+
setSnapshot: (snapshot) => {
|
|
22
|
+
set({ snapshot });
|
|
23
|
+
},
|
|
24
|
+
reset: () => {
|
|
25
|
+
set({ snapshot: createInitialCompanionShellSnapshot() });
|
|
26
|
+
}
|
|
27
|
+
}));
|
|
@@ -25,13 +25,3 @@ export type CompanionSessionSummary = {
|
|
|
25
25
|
messageCount: number;
|
|
26
26
|
status?: "idle" | "running";
|
|
27
27
|
};
|
|
28
|
-
|
|
29
|
-
export type CompanionSessionViewInput = {
|
|
30
|
-
agents: CompanionAgentProfile[];
|
|
31
|
-
sessions: CompanionSessionSummary[];
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export type CompanionAppOptions = {
|
|
35
|
-
baseUrl: string;
|
|
36
|
-
runtimeStatePath?: string;
|
|
37
|
-
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
darkMode: ["class"],
|
|
4
|
+
content: [
|
|
5
|
+
"./index.html",
|
|
6
|
+
"./src/**/*.{ts,tsx}"
|
|
7
|
+
],
|
|
8
|
+
theme: {
|
|
9
|
+
extend: {
|
|
10
|
+
colors: {
|
|
11
|
+
border: "rgb(203 213 225 / <alpha-value>)",
|
|
12
|
+
background: "rgb(248 250 252 / <alpha-value>)",
|
|
13
|
+
foreground: "rgb(15 23 42 / <alpha-value>)",
|
|
14
|
+
muted: "rgb(241 245 249 / <alpha-value>)",
|
|
15
|
+
"muted-foreground": "rgb(71 85 105 / <alpha-value>)",
|
|
16
|
+
accent: "rgb(255 255 255 / <alpha-value>)",
|
|
17
|
+
primary: "rgb(37 99 235 / <alpha-value>)",
|
|
18
|
+
success: "rgb(22 163 74 / <alpha-value>)",
|
|
19
|
+
danger: "rgb(220 38 38 / <alpha-value>)"
|
|
20
|
+
},
|
|
21
|
+
borderRadius: {
|
|
22
|
+
lg: "0.75rem",
|
|
23
|
+
xl: "1rem",
|
|
24
|
+
"2xl": "1.5rem"
|
|
25
|
+
},
|
|
26
|
+
boxShadow: {
|
|
27
|
+
companion: "0 18px 44px rgba(15, 23, 42, 0.18)"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
plugins: []
|
|
32
|
+
};
|
package/tsconfig.json
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
"module": "Node16",
|
|
7
7
|
"moduleResolution": "Node16",
|
|
8
8
|
"verbatimModuleSyntax": false,
|
|
9
|
+
"jsx": "react-jsx",
|
|
9
10
|
"types": ["node", "electron"],
|
|
10
11
|
"lib": ["DOM", "ES2022"]
|
|
11
12
|
},
|
|
12
|
-
"include": ["src/**/*.ts"]
|
|
13
|
+
"include": ["electron/**/*.ts", "src/**/*.ts", "src/**/*.tsx"]
|
|
13
14
|
}
|
package/vite.config.mts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { defineConfig } from "vite";
|
|
3
|
+
import react from "@vitejs/plugin-react";
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react()],
|
|
7
|
+
resolve: {
|
|
8
|
+
alias: {
|
|
9
|
+
"@": path.resolve(__dirname, "./src")
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
build: {
|
|
13
|
+
outDir: "dist/ui",
|
|
14
|
+
emptyOutDir: false
|
|
15
|
+
}
|
|
16
|
+
});
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const vitest_1 = require("vitest");
|
|
4
|
-
const companion_session_view_service_js_1 = require("./services/companion-session-view.service.js");
|
|
5
|
-
(0, vitest_1.describe)("CompanionSessionViewService", () => {
|
|
6
|
-
(0, vitest_1.it)("prefers the most recently updated running session", () => {
|
|
7
|
-
const service = new companion_session_view_service_js_1.CompanionSessionViewService("http://127.0.0.1:55667", (agentId) => `http://127.0.0.1:55667/api/agents/${agentId}/avatar`);
|
|
8
|
-
const view = service.selectView({
|
|
9
|
-
agents: [{ id: "writer", displayName: "Writer" }],
|
|
10
|
-
sessions: [
|
|
11
|
-
{ sessionId: "older", agentId: "writer", messageCount: 1, updatedAt: "2026-05-05T00:00:00.000Z", status: "running" },
|
|
12
|
-
{ sessionId: "newer", agentId: "writer", messageCount: 2, updatedAt: "2026-05-06T00:00:00.000Z", status: "running" }
|
|
13
|
-
]
|
|
14
|
-
});
|
|
15
|
-
(0, vitest_1.expect)(view.sessionId).toBe("newer");
|
|
16
|
-
(0, vitest_1.expect)(view.title).toBe("Writer");
|
|
17
|
-
});
|
|
18
|
-
(0, vitest_1.it)("falls back to an idle shell view when no running session exists", () => {
|
|
19
|
-
const service = new companion_session_view_service_js_1.CompanionSessionViewService("http://127.0.0.1:55667", () => "http://127.0.0.1:55667/api/agents/default/avatar");
|
|
20
|
-
const view = service.selectView({
|
|
21
|
-
agents: [],
|
|
22
|
-
sessions: []
|
|
23
|
-
});
|
|
24
|
-
(0, vitest_1.expect)(view.state).toBe("idle");
|
|
25
|
-
(0, vitest_1.expect)(view.subtitle).toBe("No active agent");
|
|
26
|
-
});
|
|
27
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CompanionSessionViewService = void 0;
|
|
4
|
-
class CompanionSessionViewService {
|
|
5
|
-
baseUrl;
|
|
6
|
-
resolveAvatarUrl;
|
|
7
|
-
constructor(baseUrl, resolveAvatarUrl) {
|
|
8
|
-
this.baseUrl = baseUrl;
|
|
9
|
-
this.resolveAvatarUrl = resolveAvatarUrl;
|
|
10
|
-
}
|
|
11
|
-
selectView = (input) => {
|
|
12
|
-
const runningSession = [...input.sessions]
|
|
13
|
-
.filter((session) => session.status === "running")
|
|
14
|
-
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0];
|
|
15
|
-
if (!runningSession) {
|
|
16
|
-
return {
|
|
17
|
-
state: "idle",
|
|
18
|
-
title: "NextClaw",
|
|
19
|
-
subtitle: "No active agent",
|
|
20
|
-
openUrl: this.baseUrl
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
const agent = this.findAgent(input.agents, runningSession.agentId);
|
|
24
|
-
return {
|
|
25
|
-
state: "running",
|
|
26
|
-
title: agent?.displayName?.trim() || runningSession.agentId || "Active Agent",
|
|
27
|
-
subtitle: runningSession.sessionId,
|
|
28
|
-
avatarUrl: runningSession.agentId ? this.resolveAvatar(agent, runningSession.agentId) : undefined,
|
|
29
|
-
sessionId: runningSession.sessionId,
|
|
30
|
-
agentId: runningSession.agentId,
|
|
31
|
-
openUrl: this.baseUrl
|
|
32
|
-
};
|
|
33
|
-
};
|
|
34
|
-
createOfflineView = (reason) => {
|
|
35
|
-
return {
|
|
36
|
-
state: "offline",
|
|
37
|
-
title: "NextClaw",
|
|
38
|
-
subtitle: reason?.summary?.trim() || "Runtime unavailable",
|
|
39
|
-
openUrl: this.baseUrl
|
|
40
|
-
};
|
|
41
|
-
};
|
|
42
|
-
findAgent = (agents, agentId) => {
|
|
43
|
-
if (!agentId) {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
return agents.find((agent) => agent.id === agentId) ?? null;
|
|
47
|
-
};
|
|
48
|
-
resolveAvatar = (agent, agentId) => {
|
|
49
|
-
return agent?.avatarUrl?.trim() || this.resolveAvatarUrl(agentId);
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
exports.CompanionSessionViewService = CompanionSessionViewService;
|