@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
|
@@ -5,12 +5,12 @@ import { resolve } from "node:path";
|
|
|
5
5
|
|
|
6
6
|
const loadModule = createRequire(__filename);
|
|
7
7
|
const electronBinary = loadModule("electron") as string;
|
|
8
|
-
const
|
|
8
|
+
const mainEntryPath = resolve(__dirname, "main.js");
|
|
9
9
|
const env = { ...process.env };
|
|
10
10
|
|
|
11
11
|
delete env.ELECTRON_RUN_AS_NODE;
|
|
12
12
|
|
|
13
|
-
const child = spawn(electronBinary, [
|
|
13
|
+
const child = spawn(electronBinary, [mainEntryPath, ...process.argv.slice(2)], {
|
|
14
14
|
stdio: "inherit",
|
|
15
15
|
env
|
|
16
16
|
});
|
package/{src → electron}/main.ts
RENAMED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CompanionAppOptions } from "./types/companion-shell.types.js";
|
|
1
2
|
import { CompanionApplicationService } from "./services/companion-application.service.js";
|
|
2
3
|
|
|
3
4
|
function resolveBaseUrl(argv: string[]): string {
|
|
@@ -13,11 +14,11 @@ function resolveBaseUrl(argv: string[]): string {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
async function main(): Promise<void> {
|
|
16
|
-
const
|
|
17
|
+
const options: CompanionAppOptions = {
|
|
17
18
|
baseUrl: resolveBaseUrl(process.argv.slice(1)),
|
|
18
19
|
runtimeStatePath: process.env.NEXTCLAW_COMPANION_RUNTIME_STATE_PATH?.trim() || undefined
|
|
19
|
-
}
|
|
20
|
-
await
|
|
20
|
+
};
|
|
21
|
+
await new CompanionApplicationService(options).run();
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
void main();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { contextBridge, ipcRenderer } from "electron";
|
|
2
|
+
|
|
3
|
+
contextBridge.exposeInMainWorld("nextclawCompanion", {
|
|
4
|
+
open: () => ipcRenderer.invoke("companion:open"),
|
|
5
|
+
quit: () => ipcRenderer.invoke("companion:quit"),
|
|
6
|
+
getBootstrap: () => ipcRenderer.invoke("companion:get-bootstrap")
|
|
7
|
+
});
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
import { app } from "electron";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { CompanionRuntimeClientService } from "./companion-runtime-client.service.js";
|
|
3
|
+
|
|
4
|
+
import type { CompanionAppOptions } from "../types/companion-shell.types.js";
|
|
5
|
+
import { CompanionRuntimeStatePersistenceService } from "./companion-runtime-state-persistence.service.js";
|
|
7
6
|
import { CompanionTrayService } from "./companion-tray.service.js";
|
|
7
|
+
import { CompanionWindowPositionPersistenceService } from "./companion-window-position-persistence.service.js";
|
|
8
8
|
import { CompanionWindowService } from "./companion-window.service.js";
|
|
9
9
|
|
|
10
10
|
export class CompanionApplicationService {
|
|
11
|
-
private readonly
|
|
12
|
-
private readonly runtimeStateStore: CompanionRuntimeStateStore | null;
|
|
11
|
+
private readonly runtimeStatePersistenceService: CompanionRuntimeStatePersistenceService | null;
|
|
13
12
|
private readonly windowService: CompanionWindowService;
|
|
14
13
|
private readonly trayService: CompanionTrayService;
|
|
15
14
|
private quitting = false;
|
|
16
15
|
|
|
17
16
|
constructor(private readonly options: CompanionAppOptions) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
this.runtimeStateStore = options.runtimeStatePath
|
|
21
|
-
? new CompanionRuntimeStateStore(options.runtimeStatePath)
|
|
17
|
+
this.runtimeStatePersistenceService = options.runtimeStatePath
|
|
18
|
+
? new CompanionRuntimeStatePersistenceService(options.runtimeStatePath)
|
|
22
19
|
: null;
|
|
23
|
-
this.windowService = new CompanionWindowService(
|
|
24
|
-
preloadPath,
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
this.windowService = new CompanionWindowService({
|
|
21
|
+
preloadPath: resolve(__dirname, "..", "preload.js"),
|
|
22
|
+
rendererEntryPath: resolve(__dirname, "..", "..", "ui", "index.html"),
|
|
23
|
+
positionPersistenceService: CompanionWindowPositionPersistenceService.fromUserData(app.getPath("userData")),
|
|
24
|
+
baseUrl: options.baseUrl,
|
|
25
|
+
onQuit: () => this.quit()
|
|
26
|
+
});
|
|
27
27
|
this.trayService = new CompanionTrayService(
|
|
28
28
|
options.baseUrl,
|
|
29
29
|
() => this.windowService.toggleVisibility(),
|
|
@@ -43,17 +43,16 @@ export class CompanionApplicationService {
|
|
|
43
43
|
app.on("window-all-closed", () => undefined);
|
|
44
44
|
app.on("before-quit", () => {
|
|
45
45
|
this.quitting = true;
|
|
46
|
-
this.runtimeClient.stop();
|
|
47
46
|
this.trayService.destroy();
|
|
48
47
|
this.windowService.destroy();
|
|
49
|
-
this.
|
|
48
|
+
this.runtimeStatePersistenceService?.clear();
|
|
50
49
|
});
|
|
51
50
|
app.on("activate", () => {
|
|
52
51
|
this.windowService.show();
|
|
53
52
|
});
|
|
54
53
|
|
|
55
54
|
await app.whenReady();
|
|
56
|
-
this.
|
|
55
|
+
this.runtimeStatePersistenceService?.write({
|
|
57
56
|
pid: process.pid,
|
|
58
57
|
startedAt: new Date().toISOString(),
|
|
59
58
|
baseUrl: this.options.baseUrl
|
|
@@ -61,9 +60,6 @@ export class CompanionApplicationService {
|
|
|
61
60
|
await this.windowService.create();
|
|
62
61
|
this.windowService.show();
|
|
63
62
|
this.trayService.create();
|
|
64
|
-
await this.runtimeClient.start((view) => {
|
|
65
|
-
this.windowService.updateView(view);
|
|
66
|
-
});
|
|
67
63
|
};
|
|
68
64
|
|
|
69
65
|
readonly quit = (): void => {
|
|
@@ -7,7 +7,7 @@ type CompanionRuntimeState = {
|
|
|
7
7
|
baseUrl: string;
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
export class
|
|
10
|
+
export class CompanionRuntimeStatePersistenceService {
|
|
11
11
|
constructor(private readonly filePath: string) {}
|
|
12
12
|
|
|
13
13
|
readonly write = (state: CompanionRuntimeState): void => {
|
|
@@ -6,7 +6,7 @@ type WindowBounds = {
|
|
|
6
6
|
y?: number;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
export class
|
|
9
|
+
export class CompanionWindowPositionPersistenceService {
|
|
10
10
|
constructor(private readonly filePath: string) {}
|
|
11
11
|
|
|
12
12
|
readonly read = (): WindowBounds | null => {
|
|
@@ -25,7 +25,7 @@ export class CompanionWindowPositionStore {
|
|
|
25
25
|
writeFileSync(this.filePath, JSON.stringify(bounds, null, 2));
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
static readonly fromUserData = (userDataPath: string):
|
|
29
|
-
return new
|
|
28
|
+
static readonly fromUserData = (userDataPath: string): CompanionWindowPositionPersistenceService => {
|
|
29
|
+
return new CompanionWindowPositionPersistenceService(resolve(userDataPath, "companion-window.json"));
|
|
30
30
|
};
|
|
31
31
|
}
|
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
import {
|
|
1
|
+
import { BrowserWindow, ipcMain, shell } from "electron";
|
|
2
|
+
|
|
3
|
+
import type { CompanionShellBootstrap } from "../types/companion-shell.types.js";
|
|
4
|
+
import type { CompanionWindowPositionPersistenceService } from "./companion-window-position-persistence.service.js";
|
|
5
5
|
|
|
6
6
|
export class CompanionWindowService {
|
|
7
7
|
private window: BrowserWindow | null = null;
|
|
8
|
-
private currentView: CompanionAvatarView | null = null;
|
|
9
8
|
|
|
10
|
-
constructor(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
constructor(private readonly options: {
|
|
10
|
+
preloadPath: string;
|
|
11
|
+
rendererEntryPath: string;
|
|
12
|
+
positionPersistenceService: CompanionWindowPositionPersistenceService;
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
onQuit: () => void;
|
|
15
|
+
}) {}
|
|
14
16
|
|
|
15
17
|
readonly create = async (): Promise<void> => {
|
|
16
18
|
if (this.window) {
|
|
17
19
|
return;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
const bounds = this.
|
|
22
|
+
const bounds = this.options.positionPersistenceService.read();
|
|
21
23
|
this.window = new BrowserWindow({
|
|
22
24
|
width: 112,
|
|
23
25
|
height: 132,
|
|
@@ -31,7 +33,7 @@ export class CompanionWindowService {
|
|
|
31
33
|
skipTaskbar: true,
|
|
32
34
|
hasShadow: false,
|
|
33
35
|
webPreferences: {
|
|
34
|
-
preload: this.preloadPath,
|
|
36
|
+
preload: this.options.preloadPath,
|
|
35
37
|
contextIsolation: true,
|
|
36
38
|
nodeIntegration: false
|
|
37
39
|
}
|
|
@@ -52,26 +54,22 @@ export class CompanionWindowService {
|
|
|
52
54
|
|
|
53
55
|
ipcMain.removeHandler("companion:open");
|
|
54
56
|
ipcMain.handle("companion:open", async () => {
|
|
55
|
-
|
|
56
|
-
if (openUrl) {
|
|
57
|
-
await shell.openExternal(openUrl);
|
|
58
|
-
}
|
|
57
|
+
await shell.openExternal(this.options.baseUrl);
|
|
59
58
|
return null;
|
|
60
59
|
});
|
|
61
60
|
ipcMain.removeHandler("companion:quit");
|
|
62
61
|
ipcMain.handle("companion:quit", async () => {
|
|
63
|
-
|
|
62
|
+
this.options.onQuit();
|
|
64
63
|
return null;
|
|
65
64
|
});
|
|
66
|
-
|
|
67
|
-
ipcMain.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
65
|
+
ipcMain.removeHandler("companion:get-bootstrap");
|
|
66
|
+
ipcMain.handle("companion:get-bootstrap", async (): Promise<CompanionShellBootstrap> => {
|
|
67
|
+
return {
|
|
68
|
+
baseUrl: this.options.baseUrl
|
|
69
|
+
};
|
|
72
70
|
});
|
|
73
71
|
|
|
74
|
-
await this.window.
|
|
72
|
+
await this.window.loadFile(this.options.rendererEntryPath);
|
|
75
73
|
};
|
|
76
74
|
|
|
77
75
|
readonly show = (): void => {
|
|
@@ -89,15 +87,10 @@ export class CompanionWindowService {
|
|
|
89
87
|
this.window.showInactive();
|
|
90
88
|
};
|
|
91
89
|
|
|
92
|
-
readonly updateView = (view: CompanionAvatarView): void => {
|
|
93
|
-
this.currentView = view;
|
|
94
|
-
this.window?.webContents.send("companion:view", view);
|
|
95
|
-
};
|
|
96
|
-
|
|
97
90
|
readonly destroy = (): void => {
|
|
98
91
|
ipcMain.removeHandler("companion:open");
|
|
99
92
|
ipcMain.removeHandler("companion:quit");
|
|
100
|
-
ipcMain.
|
|
93
|
+
ipcMain.removeHandler("companion:get-bootstrap");
|
|
101
94
|
this.window?.destroy();
|
|
102
95
|
this.window = null;
|
|
103
96
|
};
|
|
@@ -107,7 +100,7 @@ export class CompanionWindowService {
|
|
|
107
100
|
if (!bounds) {
|
|
108
101
|
return;
|
|
109
102
|
}
|
|
110
|
-
this.
|
|
103
|
+
this.options.positionPersistenceService.write({
|
|
111
104
|
x: bounds.x,
|
|
112
105
|
y: bounds.y
|
|
113
106
|
});
|
package/index.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>NextClaw Companion</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/app/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"contractKind": "legacy",
|
|
3
|
+
"organizationModel": "legacy-apps-companion-workspace-root",
|
|
4
|
+
"rootPolicy": "contract-only",
|
|
5
|
+
"enforcement": "error",
|
|
6
|
+
"allowedRootDirectories": [
|
|
7
|
+
"electron",
|
|
8
|
+
"scripts",
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"requiredRootDirectories": [
|
|
12
|
+
"electron",
|
|
13
|
+
"src"
|
|
14
|
+
],
|
|
15
|
+
"allowedRootFiles": [
|
|
16
|
+
"CHANGELOG.md",
|
|
17
|
+
"index.html",
|
|
18
|
+
"module-structure.config.json",
|
|
19
|
+
"package.json",
|
|
20
|
+
"postcss.config.mjs",
|
|
21
|
+
"tailwind.config.mjs",
|
|
22
|
+
"tsconfig.json",
|
|
23
|
+
"vite.config.mts"
|
|
24
|
+
]
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -1,31 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/companion",
|
|
3
|
-
"version": "0.1.1-beta.
|
|
3
|
+
"version": "0.1.1-beta.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Standalone Electron companion shell for active NextClaw agents.",
|
|
6
6
|
"author": "NextClaw Team",
|
|
7
7
|
"homepage": "https://github.com/Peiiii/nextclaw",
|
|
8
|
-
"main": "dist/
|
|
8
|
+
"main": "dist/electron/main.js",
|
|
9
9
|
"bin": {
|
|
10
|
-
"nextclaw-companion": "dist/
|
|
10
|
+
"nextclaw-companion": "dist/electron/launcher.js"
|
|
11
11
|
},
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"electron": "^32.2.1",
|
|
15
|
-
"
|
|
15
|
+
"lucide-react": "^0.462.0",
|
|
16
|
+
"react": "^18.3.1",
|
|
17
|
+
"react-dom": "^18.3.1",
|
|
18
|
+
"zustand": "^5.0.2",
|
|
19
|
+
"@nextclaw/client-sdk": "0.1.1-beta.2"
|
|
16
20
|
},
|
|
17
21
|
"devDependencies": {
|
|
22
|
+
"@types/react": "^18.3.12",
|
|
23
|
+
"@types/react-dom": "^18.3.1",
|
|
24
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
25
|
+
"autoprefixer": "^10.4.20",
|
|
18
26
|
"@types/node": "^20.17.6",
|
|
27
|
+
"postcss": "^8.4.49",
|
|
28
|
+
"tailwindcss": "^3.4.15",
|
|
19
29
|
"typescript": "^5.6.3",
|
|
30
|
+
"vite": "^8.0.3",
|
|
20
31
|
"vitest": "^4.1.2"
|
|
21
32
|
},
|
|
22
33
|
"scripts": {
|
|
23
|
-
"dev": "pnpm build
|
|
24
|
-
"build": "pnpm build:main",
|
|
34
|
+
"dev": "pnpm build && node dist/electron/launcher.js",
|
|
35
|
+
"build": "pnpm build:main && pnpm build:renderer",
|
|
25
36
|
"build:main": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json --outDir dist --noEmit false",
|
|
26
|
-
"
|
|
37
|
+
"build:renderer": "vite build --config vite.config.mts",
|
|
38
|
+
"lint": "eslint \"electron/**/*.ts\" \"src/**/*.{ts,tsx}\" vite.config.mts",
|
|
27
39
|
"tsc": "tsc -p tsconfig.json --noEmit",
|
|
28
|
-
"test": "vitest run src/companion-
|
|
29
|
-
"smoke": "pnpm build
|
|
40
|
+
"test": "vitest run src/managers/companion-runtime.manager.test.ts",
|
|
41
|
+
"smoke": "pnpm build && node scripts/smoke.mjs"
|
|
30
42
|
}
|
|
31
43
|
}
|
package/scripts/smoke.mjs
CHANGED
|
@@ -2,9 +2,10 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
|
|
4
4
|
const requiredFiles = [
|
|
5
|
-
resolve("dist/
|
|
6
|
-
resolve("dist/
|
|
7
|
-
resolve("dist/
|
|
5
|
+
resolve("dist/electron/main.js"),
|
|
6
|
+
resolve("dist/electron/launcher.js"),
|
|
7
|
+
resolve("dist/electron/preload.js"),
|
|
8
|
+
resolve("dist/ui/index.html")
|
|
8
9
|
];
|
|
9
10
|
|
|
10
11
|
const missingFiles = requiredFiles.filter((filePath) => !existsSync(filePath));
|
package/src/app/app.tsx
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
import { CompanionShellContainer } from "./components/companion-shell.container.js";
|
|
4
|
+
import { usePresenter } from "./providers/companion-presenter.provider.js";
|
|
5
|
+
|
|
6
|
+
export function App() {
|
|
7
|
+
const presenter = usePresenter();
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
void presenter.bootstrap();
|
|
11
|
+
return () => {
|
|
12
|
+
presenter.shutdown();
|
|
13
|
+
};
|
|
14
|
+
}, [presenter]);
|
|
15
|
+
|
|
16
|
+
return <CompanionShellContainer />;
|
|
17
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ExternalLink, X } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import { useCompanionRuntimeStore } from "../../stores/companion-runtime.store.js";
|
|
4
|
+
import { useCompanionShellStore } from "../../stores/companion-shell.store.js";
|
|
5
|
+
import { usePresenter } from "../providers/companion-presenter.provider.js";
|
|
6
|
+
|
|
7
|
+
export function CompanionShellContainer() {
|
|
8
|
+
const presenter = usePresenter();
|
|
9
|
+
const view = useCompanionRuntimeStore((state) => state.snapshot.currentView);
|
|
10
|
+
const connectionState = useCompanionRuntimeStore((state) => state.snapshot.connectionState);
|
|
11
|
+
const bootstrapped = useCompanionShellStore((state) => state.snapshot.bootstrapped);
|
|
12
|
+
|
|
13
|
+
const statusColorClass = connectionState === "running"
|
|
14
|
+
? "bg-success"
|
|
15
|
+
: connectionState === "offline"
|
|
16
|
+
? "bg-danger"
|
|
17
|
+
: "bg-slate-300";
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="h-screen w-screen overflow-hidden bg-transparent">
|
|
21
|
+
<div className="grid h-full place-items-center">
|
|
22
|
+
<div className="grid w-[112px] justify-items-center gap-2">
|
|
23
|
+
<div className="drag-region relative grid h-24 w-24 place-items-center overflow-hidden rounded-[28px] border border-white/70 bg-white/90 shadow-companion backdrop-blur-xl">
|
|
24
|
+
<div className="no-drag absolute right-1.5 top-1.5 z-20 flex gap-1">
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
aria-label="Open NextClaw"
|
|
28
|
+
onClick={() => void presenter.companionShellManager.open()}
|
|
29
|
+
className="grid h-5 w-5 place-items-center rounded-full bg-slate-900/10 text-slate-700 transition hover:bg-slate-900/15"
|
|
30
|
+
>
|
|
31
|
+
<ExternalLink className="h-3 w-3" />
|
|
32
|
+
</button>
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
aria-label="Quit Companion"
|
|
36
|
+
onClick={() => void presenter.companionShellManager.quit()}
|
|
37
|
+
className="grid h-5 w-5 place-items-center rounded-full bg-slate-900/10 text-slate-700 transition hover:bg-slate-900/15"
|
|
38
|
+
>
|
|
39
|
+
<X className="h-3 w-3" />
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
{view.avatarUrl ? (
|
|
43
|
+
<img src={view.avatarUrl} alt="" className="h-full w-full object-cover" />
|
|
44
|
+
) : (
|
|
45
|
+
<span className="grid h-full w-full place-items-center bg-gradient-to-b from-white to-slate-200 text-[28px] font-bold text-slate-800">
|
|
46
|
+
{(view.title || "NC").slice(0, 2).toUpperCase()}
|
|
47
|
+
</span>
|
|
48
|
+
)}
|
|
49
|
+
<span className={`absolute bottom-2 right-2 h-3.5 w-3.5 rounded-full border-2 border-white ${statusColorClass}`} />
|
|
50
|
+
</div>
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
onClick={() => void presenter.companionShellManager.open()}
|
|
54
|
+
className="no-drag w-full rounded-xl px-1 text-center"
|
|
55
|
+
>
|
|
56
|
+
<div className="truncate text-[12px] font-semibold leading-4 text-slate-900">
|
|
57
|
+
{bootstrapped ? view.title : "NextClaw"}
|
|
58
|
+
</div>
|
|
59
|
+
<div className="truncate pt-0.5 text-[11px] leading-4 text-slate-500">
|
|
60
|
+
{bootstrapped ? view.subtitle : "Connecting"}
|
|
61
|
+
</div>
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
* {
|
|
7
|
+
box-sizing: border-box;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
html,
|
|
11
|
+
body,
|
|
12
|
+
#root {
|
|
13
|
+
margin: 0;
|
|
14
|
+
width: 100%;
|
|
15
|
+
height: 100%;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
background: transparent;
|
|
18
|
+
color: rgb(15 23 42);
|
|
19
|
+
font-family: "SF Pro Display", "Inter", "Helvetica Neue", sans-serif;
|
|
20
|
+
-webkit-font-smoothing: antialiased;
|
|
21
|
+
-moz-osx-font-smoothing: grayscale;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
button {
|
|
25
|
+
font: inherit;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@layer utilities {
|
|
30
|
+
.drag-region {
|
|
31
|
+
-webkit-app-region: drag;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.no-drag {
|
|
35
|
+
-webkit-app-region: no-drag;
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/app/main.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { StrictMode } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
|
|
4
|
+
import { App } from "./app.js";
|
|
5
|
+
import { CompanionPresenterProvider } from "./providers/companion-presenter.provider.js";
|
|
6
|
+
import { companionPresenter } from "../presenters/companion.presenter.js";
|
|
7
|
+
import "./index.css";
|
|
8
|
+
|
|
9
|
+
createRoot(document.getElementById("root") as HTMLElement).render(
|
|
10
|
+
<StrictMode>
|
|
11
|
+
<CompanionPresenterProvider presenter={companionPresenter}>
|
|
12
|
+
<App />
|
|
13
|
+
</CompanionPresenterProvider>
|
|
14
|
+
</StrictMode>
|
|
15
|
+
);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import type { CompanionPresenter } from "../../presenters/companion.presenter.js";
|
|
4
|
+
|
|
5
|
+
const CompanionPresenterContext = createContext<CompanionPresenter | null>(null);
|
|
6
|
+
|
|
7
|
+
export function CompanionPresenterProvider({
|
|
8
|
+
presenter,
|
|
9
|
+
children
|
|
10
|
+
}: {
|
|
11
|
+
presenter: CompanionPresenter;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<CompanionPresenterContext.Provider value={presenter}>
|
|
16
|
+
{children}
|
|
17
|
+
</CompanionPresenterContext.Provider>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function usePresenter(): CompanionPresenter {
|
|
22
|
+
const presenter = useContext(CompanionPresenterContext);
|
|
23
|
+
if (!presenter) {
|
|
24
|
+
throw new Error("usePresenter must be used inside CompanionPresenterProvider");
|
|
25
|
+
}
|
|
26
|
+
return presenter;
|
|
27
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { useCompanionRuntimeStore } from "../stores/companion-runtime.store.js";
|
|
4
|
+
import { CompanionRuntimeManager } from "./companion-runtime.manager.js";
|
|
5
|
+
|
|
6
|
+
describe("CompanionRuntimeManager", () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
useCompanionRuntimeStore.getState().reset();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("prefers the most recently updated running session", () => {
|
|
12
|
+
const manager = new CompanionRuntimeManager();
|
|
13
|
+
const baseUrl = "http://127.0.0.1:55667";
|
|
14
|
+
|
|
15
|
+
(manager as unknown as { baseUrl: string | null }).baseUrl = baseUrl;
|
|
16
|
+
|
|
17
|
+
const snapshot = manager.syncRuntimeSnapshot({
|
|
18
|
+
agents: [{ id: "writer", displayName: "Writer" }],
|
|
19
|
+
sessions: [
|
|
20
|
+
{ sessionId: "older", agentId: "writer", messageCount: 1, updatedAt: "2026-05-05T00:00:00.000Z", status: "running" },
|
|
21
|
+
{ sessionId: "newer", agentId: "writer", messageCount: 2, updatedAt: "2026-05-06T00:00:00.000Z", status: "running" }
|
|
22
|
+
]
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(snapshot.currentView.sessionId).toBe("newer");
|
|
26
|
+
expect(snapshot.currentView.title).toBe("Writer");
|
|
27
|
+
expect(snapshot.connectionState).toBe("running");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("falls back to an idle shell view when no running session exists", () => {
|
|
31
|
+
const manager = new CompanionRuntimeManager();
|
|
32
|
+
const baseUrl = "http://127.0.0.1:55667";
|
|
33
|
+
|
|
34
|
+
(manager as unknown as { baseUrl: string | null }).baseUrl = baseUrl;
|
|
35
|
+
|
|
36
|
+
const snapshot = manager.syncRuntimeSnapshot({
|
|
37
|
+
agents: [],
|
|
38
|
+
sessions: []
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(snapshot.currentView.state).toBe("idle");
|
|
42
|
+
expect(snapshot.currentView.subtitle).toBe("No active agent");
|
|
43
|
+
expect(snapshot.connectionState).toBe("idle");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("produces an offline snapshot with a readable reason", () => {
|
|
47
|
+
const manager = new CompanionRuntimeManager();
|
|
48
|
+
(manager as unknown as { baseUrl: string | null }).baseUrl = "http://127.0.0.1:55667";
|
|
49
|
+
|
|
50
|
+
const snapshot = manager.applyOfflineState({ summary: "Cannot reach runtime" });
|
|
51
|
+
|
|
52
|
+
expect(snapshot.connectionState).toBe("offline");
|
|
53
|
+
expect(snapshot.currentView.state).toBe("offline");
|
|
54
|
+
expect(snapshot.currentView.subtitle).toBe("Cannot reach runtime");
|
|
55
|
+
});
|
|
56
|
+
});
|