@codex-app-sdk/create-codex-app 0.0.0-stage → 0.14.1
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/LICENSE +201 -0
- package/README.md +21 -2
- package/bin/create-codex-app.js +5 -0
- package/package.json +36 -4
- package/src/cli.js +245 -0
- package/template/README.md +27 -0
- package/template/_gitignore +5 -0
- package/template/package.json +30 -0
- package/template/src/main/index.ts +95 -0
- package/template/src/main/preload.ts +4 -0
- package/template/src/renderer/App.vue +90 -0
- package/template/src/renderer/index.html +16 -0
- package/template/src/renderer/main.ts +11 -0
- package/template/src/renderer/styles.css +204 -0
- package/template/src/renderer/vite-env.d.ts +12 -0
- package/template/tsconfig.json +16 -0
- package/template/vite.config.ts +62 -0
- package/template-web/README.md +30 -0
- package/template-web/_gitignore +4 -0
- package/template-web/package.json +31 -0
- package/template-web/src/client/App.vue +36 -0
- package/template-web/src/client/index.html +16 -0
- package/template-web/src/client/main.ts +6 -0
- package/template-web/src/client/styles.css +44 -0
- package/template-web/src/client/vite-env.d.ts +1 -0
- package/template-web/src/server/index.ts +81 -0
- package/template-web/tsconfig.json +14 -0
- package/template-web/tsconfig.server.json +15 -0
- package/template-web/vite.config.ts +14 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue';
|
|
3
|
+
import { CodexConversationPane, useCodexSurface } from '@codex-app-sdk/vue';
|
|
4
|
+
|
|
5
|
+
const surface = useCodexSurface(window.codexSurface);
|
|
6
|
+
const localSignInError = ref<string | null>(null);
|
|
7
|
+
const requiresSignIn = computed(() => (
|
|
8
|
+
surface.state.authentication.account === null
|
|
9
|
+
&& surface.state.authentication.requiresOpenaiAuth === true
|
|
10
|
+
));
|
|
11
|
+
const signInBusy = computed(() => (
|
|
12
|
+
surface.state.authentication.login.status === 'starting'
|
|
13
|
+
|| surface.state.authentication.login.status === 'pending'
|
|
14
|
+
));
|
|
15
|
+
const signInError = computed(() => (
|
|
16
|
+
localSignInError.value
|
|
17
|
+
?? surface.state.authentication.login.error
|
|
18
|
+
?? surface.state.authentication.error
|
|
19
|
+
));
|
|
20
|
+
|
|
21
|
+
async function signIn(): Promise<void> {
|
|
22
|
+
localSignInError.value = null;
|
|
23
|
+
let loginId: string | undefined;
|
|
24
|
+
try {
|
|
25
|
+
const login = await surface.startChatGptLogin();
|
|
26
|
+
loginId = login.loginId;
|
|
27
|
+
await window.codexAppSdkNative.openExternal(login.authUrl);
|
|
28
|
+
} catch {
|
|
29
|
+
if (loginId) await surface.cancelLogin(loginId).catch(() => undefined);
|
|
30
|
+
localSignInError.value = 'Could not open Codex sign in. Please try again.';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<template>
|
|
36
|
+
<main class="app-shell">
|
|
37
|
+
<aside class="sidebar" aria-label="Conversations">
|
|
38
|
+
<header class="sidebar__header">
|
|
39
|
+
<strong class="sidebar__brand">{{displayName}}</strong>
|
|
40
|
+
</header>
|
|
41
|
+
<div class="sidebar__actions">
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
class="sidebar__new-chat"
|
|
45
|
+
:disabled="surface.state.status !== 'ready'"
|
|
46
|
+
@click="surface.createConversation()"
|
|
47
|
+
>
|
|
48
|
+
<svg aria-hidden="true" viewBox="0 0 24 24">
|
|
49
|
+
<path d="M12 20h9" />
|
|
50
|
+
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L8 18l-4 1 1-4Z" />
|
|
51
|
+
</svg>
|
|
52
|
+
<span>New chat</span>
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<section class="sidebar__recents" aria-labelledby="recent-chats-heading">
|
|
57
|
+
<h2 id="recent-chats-heading" class="sidebar__section-title">Recent chats</h2>
|
|
58
|
+
<p v-if="surface.state.conversations.length === 0" class="sidebar__empty">
|
|
59
|
+
{{ surface.state.status === 'connecting' ? 'Loading…' : 'No conversations yet' }}
|
|
60
|
+
</p>
|
|
61
|
+
<nav v-else class="sidebar__list" aria-label="Recent chats">
|
|
62
|
+
<button
|
|
63
|
+
v-for="conversation in surface.state.conversations"
|
|
64
|
+
:key="conversation.id"
|
|
65
|
+
type="button"
|
|
66
|
+
class="sidebar__conversation"
|
|
67
|
+
:class="{ 'sidebar__conversation--active': conversation.id === surface.state.activeConversationId }"
|
|
68
|
+
:title="conversation.title || 'Untitled conversation'"
|
|
69
|
+
@click="surface.selectConversation(conversation.id)"
|
|
70
|
+
>
|
|
71
|
+
<span>{{ conversation.title || 'Untitled conversation' }}</span>
|
|
72
|
+
</button>
|
|
73
|
+
</nav>
|
|
74
|
+
</section>
|
|
75
|
+
</aside>
|
|
76
|
+
|
|
77
|
+
<section v-if="requiresSignIn" class="auth-landing">
|
|
78
|
+
<div class="auth-landing__card">
|
|
79
|
+
<span class="auth-landing__mark" aria-hidden="true">✦</span>
|
|
80
|
+
<h1>Connect Codex</h1>
|
|
81
|
+
<p>Sign in to start conversations and build with Codex in this app.</p>
|
|
82
|
+
<button type="button" :disabled="signInBusy" @click="signIn">
|
|
83
|
+
{{ signInBusy ? 'Waiting for sign in…' : 'Sign in with ChatGPT' }}
|
|
84
|
+
</button>
|
|
85
|
+
<p v-if="signInError" class="auth-landing__error" role="alert">{{ signInError }}</p>
|
|
86
|
+
</div>
|
|
87
|
+
</section>
|
|
88
|
+
<CodexConversationPane v-else :surface="surface" autofocus />
|
|
89
|
+
</main>
|
|
90
|
+
</template>
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
<meta
|
|
7
|
+
http-equiv="Content-Security-Policy"
|
|
8
|
+
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; media-src 'self' blob:; font-src 'self' data:; connect-src 'self' ws://127.0.0.1:* ws://localhost:*; worker-src 'self' blob:; object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'"
|
|
9
|
+
/>
|
|
10
|
+
<title>{{displayName}}</title>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="app"></div>
|
|
14
|
+
<script type="module" src="/main.ts"></script>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createApp } from 'vue';
|
|
2
|
+
import '@codex-app-sdk/vue/styles.css';
|
|
3
|
+
import App from './App.vue';
|
|
4
|
+
import './styles.css';
|
|
5
|
+
|
|
6
|
+
document.documentElement.dataset.platform = navigator.platform.startsWith('Mac')
|
|
7
|
+
|| navigator.userAgent.includes('Macintosh')
|
|
8
|
+
? 'macos'
|
|
9
|
+
: 'other';
|
|
10
|
+
|
|
11
|
+
createApp(App).mount('#app');
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color: #20211f;
|
|
3
|
+
background: #f7f7f5;
|
|
4
|
+
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
5
|
+
font-synthesis: none;
|
|
6
|
+
--codex-surface-color: #fff;
|
|
7
|
+
--codex-composer-background: #fff;
|
|
8
|
+
--codex-border-color: #deded9;
|
|
9
|
+
--codex-message-list-content-width: 760px;
|
|
10
|
+
--codex-composer-content-width: 760px;
|
|
11
|
+
--color-surface-lowest: #fff;
|
|
12
|
+
--color-surface-low: #f4f4f4;
|
|
13
|
+
--color-shell-main: var(--color-surface-lowest);
|
|
14
|
+
--shell-glass-opacity: 65%;
|
|
15
|
+
--color-shell-sidebar: var(--color-surface-low);
|
|
16
|
+
--color-shell-appbar-divider: color-mix(in srgb, #b0b0b0 14%, transparent);
|
|
17
|
+
--workbench-appbar-height: 48px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
html[data-platform='macos'] {
|
|
21
|
+
background: transparent;
|
|
22
|
+
--color-shell-sidebar: color-mix(in srgb, var(--color-shell-main) var(--shell-glass-opacity), transparent);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
* { box-sizing: border-box; }
|
|
26
|
+
html, body, #app { width: 100%; height: 100%; margin: 0; }
|
|
27
|
+
button { font: inherit; }
|
|
28
|
+
|
|
29
|
+
.app-shell {
|
|
30
|
+
display: grid;
|
|
31
|
+
width: 100%;
|
|
32
|
+
height: 100%;
|
|
33
|
+
grid-template-columns: 248px minmax(0, 1fr);
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
background: transparent;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.sidebar {
|
|
39
|
+
display: flex;
|
|
40
|
+
min-height: 0;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
border-right: 1px solid #deded9;
|
|
43
|
+
background: var(--color-shell-sidebar);
|
|
44
|
+
user-select: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.sidebar__header {
|
|
48
|
+
display: flex;
|
|
49
|
+
min-width: 0;
|
|
50
|
+
height: var(--workbench-appbar-height);
|
|
51
|
+
flex: 0 0 var(--workbench-appbar-height);
|
|
52
|
+
align-items: center;
|
|
53
|
+
padding: 0 12px 0 20px;
|
|
54
|
+
border-bottom: 1px solid var(--color-shell-appbar-divider);
|
|
55
|
+
-webkit-app-region: drag;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
html[data-platform='macos'] .sidebar__header {
|
|
59
|
+
padding-left: 88px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.sidebar__brand {
|
|
63
|
+
min-width: 0;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
font-size: 14px;
|
|
66
|
+
font-weight: 650;
|
|
67
|
+
line-height: 20px;
|
|
68
|
+
text-overflow: ellipsis;
|
|
69
|
+
white-space: nowrap;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.sidebar__actions {
|
|
73
|
+
padding: 10px 8px 4px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.sidebar__actions .sidebar__new-chat {
|
|
77
|
+
width: 100%;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.sidebar button {
|
|
81
|
+
display: flex;
|
|
82
|
+
min-width: 0;
|
|
83
|
+
align-items: center;
|
|
84
|
+
border: 0;
|
|
85
|
+
border-radius: 8px;
|
|
86
|
+
padding: 7px 10px;
|
|
87
|
+
color: #2a2b29;
|
|
88
|
+
background: transparent;
|
|
89
|
+
font-size: 13px;
|
|
90
|
+
line-height: 20px;
|
|
91
|
+
text-align: left;
|
|
92
|
+
cursor: pointer;
|
|
93
|
+
-webkit-app-region: no-drag;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.sidebar button.sidebar__new-chat {
|
|
97
|
+
gap: 9px;
|
|
98
|
+
font-size: 14px;
|
|
99
|
+
font-weight: 500;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.sidebar__new-chat svg {
|
|
103
|
+
width: 17px;
|
|
104
|
+
height: 17px;
|
|
105
|
+
flex: 0 0 auto;
|
|
106
|
+
fill: none;
|
|
107
|
+
stroke: currentColor;
|
|
108
|
+
stroke-linecap: round;
|
|
109
|
+
stroke-linejoin: round;
|
|
110
|
+
stroke-width: 1.8;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.sidebar button:hover,
|
|
114
|
+
.sidebar button:focus-visible,
|
|
115
|
+
.sidebar__conversation--active { background: #e2e2dd; }
|
|
116
|
+
.sidebar button:focus-visible { outline: 2px solid #3b82f6; outline-offset: -2px; }
|
|
117
|
+
.sidebar button:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
118
|
+
|
|
119
|
+
.sidebar__recents {
|
|
120
|
+
display: flex;
|
|
121
|
+
min-height: 0;
|
|
122
|
+
flex-direction: column;
|
|
123
|
+
flex: 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.sidebar__section-title {
|
|
127
|
+
margin: 6px 20px 7px;
|
|
128
|
+
color: #85857e;
|
|
129
|
+
font-size: 11px;
|
|
130
|
+
font-weight: 650;
|
|
131
|
+
line-height: 16px;
|
|
132
|
+
letter-spacing: 0.02em;
|
|
133
|
+
text-transform: uppercase;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.sidebar__list {
|
|
137
|
+
display: flex;
|
|
138
|
+
min-height: 0;
|
|
139
|
+
padding: 0 8px 12px;
|
|
140
|
+
overflow-y: auto;
|
|
141
|
+
flex-direction: column;
|
|
142
|
+
gap: 1px;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.sidebar__conversation span {
|
|
146
|
+
min-width: 0;
|
|
147
|
+
overflow: hidden;
|
|
148
|
+
text-overflow: ellipsis;
|
|
149
|
+
white-space: nowrap;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.sidebar__conversation--active { font-weight: 600; }
|
|
153
|
+
.sidebar__empty { margin: 0; padding: 5px 20px; color: #85857e; font-size: 12px; }
|
|
154
|
+
|
|
155
|
+
.auth-landing {
|
|
156
|
+
display: grid;
|
|
157
|
+
min-width: 0;
|
|
158
|
+
min-height: 0;
|
|
159
|
+
place-items: center;
|
|
160
|
+
padding: 40px;
|
|
161
|
+
background: var(--color-shell-main);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.auth-landing__card {
|
|
165
|
+
display: grid;
|
|
166
|
+
width: min(420px, 100%);
|
|
167
|
+
justify-items: center;
|
|
168
|
+
border: 1px solid var(--codex-border-color);
|
|
169
|
+
border-radius: 18px;
|
|
170
|
+
padding: 34px;
|
|
171
|
+
background: #fff;
|
|
172
|
+
box-shadow: 0 14px 36px rgb(0 0 0 / 6%);
|
|
173
|
+
text-align: center;
|
|
174
|
+
}
|
|
175
|
+
.auth-landing__mark {
|
|
176
|
+
display: grid;
|
|
177
|
+
width: 44px;
|
|
178
|
+
height: 44px;
|
|
179
|
+
place-items: center;
|
|
180
|
+
border-radius: 14px;
|
|
181
|
+
color: #4f46e5;
|
|
182
|
+
background: #eeedff;
|
|
183
|
+
font-size: 24px;
|
|
184
|
+
}
|
|
185
|
+
.auth-landing h1 { margin: 16px 0 0; font-size: 24px; }
|
|
186
|
+
.auth-landing p { margin: 9px 0 0; color: #6a6b66; font-size: 14px; line-height: 1.5; }
|
|
187
|
+
.auth-landing button {
|
|
188
|
+
min-height: 42px;
|
|
189
|
+
margin-top: 24px;
|
|
190
|
+
border: 0;
|
|
191
|
+
border-radius: 10px;
|
|
192
|
+
padding: 0 20px;
|
|
193
|
+
color: #fff;
|
|
194
|
+
background: #4f46e5;
|
|
195
|
+
font-weight: 650;
|
|
196
|
+
cursor: pointer;
|
|
197
|
+
}
|
|
198
|
+
.auth-landing button:hover:not(:disabled) { background: #4338ca; }
|
|
199
|
+
.auth-landing button:disabled { cursor: wait; opacity: 0.65; }
|
|
200
|
+
.auth-landing .auth-landing__error { color: #b42318; font-size: 12px; }
|
|
201
|
+
|
|
202
|
+
@media (max-width: 720px) {
|
|
203
|
+
.app-shell { grid-template-columns: 190px minmax(0, 1fr); }
|
|
204
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
import type { CodexNativeRendererApi, CodexSurfaceRendererApi } from '@codex-app-sdk/electron';
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
interface Window {
|
|
7
|
+
codexAppSdkNative: CodexNativeRendererApi;
|
|
8
|
+
codexSurface: CodexSurfaceRendererApi;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
13
|
+
"types": ["node"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { fileURLToPath, URL } from 'node:url';
|
|
2
|
+
import vue from '@vitejs/plugin-vue';
|
|
3
|
+
import { defineConfig } from 'vite';
|
|
4
|
+
import electron from 'vite-plugin-electron/simple';
|
|
5
|
+
|
|
6
|
+
const projectRoot = fileURLToPath(new URL('.', import.meta.url));
|
|
7
|
+
const mainEntry = fileURLToPath(new URL('./src/main/index.ts', import.meta.url));
|
|
8
|
+
const preloadEntry = fileURLToPath(new URL('./src/main/preload.ts', import.meta.url));
|
|
9
|
+
const mainOutDir = fileURLToPath(new URL('./dist-main', import.meta.url));
|
|
10
|
+
const rendererRoot = fileURLToPath(new URL('./src/renderer', import.meta.url));
|
|
11
|
+
const rendererOutDir = fileURLToPath(new URL('./dist-renderer', import.meta.url));
|
|
12
|
+
|
|
13
|
+
export default defineConfig(({ command, mode }) => ({
|
|
14
|
+
base: './',
|
|
15
|
+
root: rendererRoot,
|
|
16
|
+
plugins: [
|
|
17
|
+
vue(),
|
|
18
|
+
(command === 'build' || mode === 'electron') && electron({
|
|
19
|
+
main: {
|
|
20
|
+
entry: mainEntry,
|
|
21
|
+
onstart: async ({ startup }) => {
|
|
22
|
+
await startup(['.'], { cwd: projectRoot });
|
|
23
|
+
},
|
|
24
|
+
vite: {
|
|
25
|
+
root: projectRoot,
|
|
26
|
+
build: {
|
|
27
|
+
outDir: mainOutDir,
|
|
28
|
+
emptyOutDir: command === 'build',
|
|
29
|
+
lib: { entry: mainEntry, fileName: () => 'main.js' },
|
|
30
|
+
rolldownOptions: { external: command === 'build' ? [/^@codex-app-sdk\//] : [] },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
preload: {
|
|
35
|
+
input: preloadEntry,
|
|
36
|
+
vite: {
|
|
37
|
+
root: projectRoot,
|
|
38
|
+
build: {
|
|
39
|
+
outDir: mainOutDir,
|
|
40
|
+
emptyOutDir: false,
|
|
41
|
+
rolldownOptions: {
|
|
42
|
+
output: {
|
|
43
|
+
format: 'cjs',
|
|
44
|
+
codeSplitting: false,
|
|
45
|
+
entryFileNames: 'preload.cjs',
|
|
46
|
+
chunkFileNames: '[name].cjs',
|
|
47
|
+
assetFileNames: '[name].[ext]',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
],
|
|
55
|
+
server: { host: '127.0.0.1', port: 5173, strictPort: true },
|
|
56
|
+
build: {
|
|
57
|
+
outDir: rendererOutDir,
|
|
58
|
+
emptyOutDir: true,
|
|
59
|
+
// The stock conversation pane includes local syntax-highlighting grammars.
|
|
60
|
+
chunkSizeWarningLimit: 4_000,
|
|
61
|
+
},
|
|
62
|
+
}));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# {{displayName}}
|
|
2
|
+
|
|
3
|
+
Express, `ws`, and Vue application powered by [Codex App SDK](https://github.com/codex-app-sdk/codex-app-sdk).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install
|
|
7
|
+
npm run dev
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Open `http://127.0.0.1:3000`. The starter uses the default Codex home and its
|
|
11
|
+
existing local authentication.
|
|
12
|
+
|
|
13
|
+
The generated renderer contains only the SDK web client and stock conversation
|
|
14
|
+
sidebar/pane wiring. Express owns HTTP and site authentication; `ws` owns the
|
|
15
|
+
upgrade; the SDK owns protocol framing and surface routing. Replace
|
|
16
|
+
`authenticateSiteRequest()` and `acquireCodexSession()` with your website's
|
|
17
|
+
session lookup and per-user backend/process pool. Stable `codexHome`
|
|
18
|
+
directories, stored tokens, quotas, and persistence remain host concerns.
|
|
19
|
+
|
|
20
|
+
The SDK transport is framework-neutral; Express and `ws` make the generated
|
|
21
|
+
boundary runnable but are not dependencies of `@codex-app-sdk/web`. See the
|
|
22
|
+
[web integration guide](https://codex-app-sdk.github.io/codex-app-sdk/guide/web.html)
|
|
23
|
+
before replacing the demo authorization seams.
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
- `npm run dev` builds and starts the local server.
|
|
28
|
+
- `npm run build` creates the browser and server bundles.
|
|
29
|
+
- `npm start` builds and starts the application.
|
|
30
|
+
- `npm run typecheck` checks the TypeScript and Vue source.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{packageName}}",
|
|
3
|
+
"productName": "{{displayName}}",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"private": true,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "npm run build && node dist/server/index.js",
|
|
9
|
+
"build": "vite build && tsc -p tsconfig.server.json",
|
|
10
|
+
"start": "npm run build && node dist/server/index.js",
|
|
11
|
+
"typecheck": "vue-tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.server.json"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@codex-app-sdk/backend": "^0.14.1",
|
|
15
|
+
"@codex-app-sdk/core": "^0.14.1",
|
|
16
|
+
"@codex-app-sdk/vue": "^0.14.1",
|
|
17
|
+
"@codex-app-sdk/web": "^0.14.1",
|
|
18
|
+
"express": "^5.1.0",
|
|
19
|
+
"vue": "^3.5.0",
|
|
20
|
+
"ws": "^8.18.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/express": "^5.0.0",
|
|
24
|
+
"@types/node": "^22.0.0",
|
|
25
|
+
"@types/ws": "^8.18.0",
|
|
26
|
+
"@vitejs/plugin-vue": "^6.0.0",
|
|
27
|
+
"typescript": "^6.0.0",
|
|
28
|
+
"vite": "^8.0.0",
|
|
29
|
+
"vue-tsc": "^3.3.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<main class="web-app">
|
|
3
|
+
<CodexConversationSidebar
|
|
4
|
+
brand="{{displayName}}"
|
|
5
|
+
:active-conversation-id="surface.state.activeConversationId"
|
|
6
|
+
:conversations="surface.state.conversations"
|
|
7
|
+
:create-disabled="surface.state.status !== 'ready'"
|
|
8
|
+
:loading="surface.state.status === 'connecting'"
|
|
9
|
+
@create="surface.createConversation()"
|
|
10
|
+
@delete="surface.deleteConversation($event)"
|
|
11
|
+
@select="surface.selectConversation($event)"
|
|
12
|
+
/>
|
|
13
|
+
<CodexConversationPane :surface="surface" autofocus />
|
|
14
|
+
</main>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import {
|
|
19
|
+
CodexConversationPane,
|
|
20
|
+
CodexConversationSidebar,
|
|
21
|
+
useCodexSurface,
|
|
22
|
+
} from '@codex-app-sdk/vue';
|
|
23
|
+
import {
|
|
24
|
+
createCodexBrowserWebSocketPort,
|
|
25
|
+
createCodexWebSurfaceClient,
|
|
26
|
+
} from '@codex-app-sdk/web/client';
|
|
27
|
+
|
|
28
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
29
|
+
const api = createCodexWebSurfaceClient({
|
|
30
|
+
createSocket: () => createCodexBrowserWebSocketPort(
|
|
31
|
+
new WebSocket(`${protocol}//${window.location.host}/codex`),
|
|
32
|
+
),
|
|
33
|
+
reconnect: true,
|
|
34
|
+
});
|
|
35
|
+
const surface = useCodexSurface(api);
|
|
36
|
+
</script>
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
<meta
|
|
7
|
+
http-equiv="Content-Security-Policy"
|
|
8
|
+
content="default-src 'self'; connect-src 'self' ws: wss:; font-src 'self' data:; img-src 'self' data: blob: https:; style-src 'self' 'unsafe-inline'; script-src 'self'"
|
|
9
|
+
/>
|
|
10
|
+
<title>{{displayName}}</title>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="app"></div>
|
|
14
|
+
<script type="module" src="/main.ts"></script>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color: #20211f;
|
|
3
|
+
background: #f7f7f5;
|
|
4
|
+
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
5
|
+
font-synthesis: none;
|
|
6
|
+
--codex-surface-color: #fff;
|
|
7
|
+
--codex-composer-background: #fff;
|
|
8
|
+
--codex-border-color: #deded9;
|
|
9
|
+
--codex-message-list-content-width: 760px;
|
|
10
|
+
--codex-composer-content-width: 760px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
* {
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
html,
|
|
18
|
+
body,
|
|
19
|
+
#app,
|
|
20
|
+
.web-app {
|
|
21
|
+
width: 100%;
|
|
22
|
+
height: 100%;
|
|
23
|
+
margin: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
body {
|
|
27
|
+
background: #f7f7f5;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
button,
|
|
31
|
+
textarea {
|
|
32
|
+
font: inherit;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.web-app {
|
|
36
|
+
display: grid;
|
|
37
|
+
min-width: 320px;
|
|
38
|
+
grid-template-columns: 260px minmax(0, 1fr);
|
|
39
|
+
overflow: hidden;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@media (max-width: 760px) {
|
|
43
|
+
.web-app { grid-template-columns: 210px minmax(0, 1fr); }
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createServer, type IncomingMessage } from 'node:http';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import express from 'express';
|
|
5
|
+
import { WebSocketServer } from 'ws';
|
|
6
|
+
import { createCodexAppBackend } from '@codex-app-sdk/backend';
|
|
7
|
+
import {
|
|
8
|
+
bindCodexWebSocket,
|
|
9
|
+
createCodexNodeWebSocketPort,
|
|
10
|
+
isAllowedCodexWebSocketOrigin,
|
|
11
|
+
} from '@codex-app-sdk/web/server';
|
|
12
|
+
|
|
13
|
+
type SiteUser = { id: string };
|
|
14
|
+
type WebSessionContext = { request: IncomingMessage; siteUser: SiteUser };
|
|
15
|
+
|
|
16
|
+
const port = Number(process.env.PORT ?? 3000);
|
|
17
|
+
// Browsers let any website open a WebSocket to this server, so only pages
|
|
18
|
+
// served from these origins may drive Codex. Set ALLOWED_ORIGINS
|
|
19
|
+
// (comma-separated) to the public origin when deploying.
|
|
20
|
+
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',').map((origin) => origin.trim()).filter(Boolean)
|
|
21
|
+
?? [`http://127.0.0.1:${port}`, `http://localhost:${port}`];
|
|
22
|
+
const clientDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../client');
|
|
23
|
+
const app = express();
|
|
24
|
+
const httpServer = createServer(app);
|
|
25
|
+
const webSocketServer = new WebSocketServer({ noServer: true });
|
|
26
|
+
const backend = createCodexAppBackend({
|
|
27
|
+
surfaceOptions: { autoSelectFirstConversation: false },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
app.use(express.static(clientDirectory));
|
|
31
|
+
app.get('/', (_request, response) => response.sendFile(path.join(clientDirectory, 'index.html')));
|
|
32
|
+
|
|
33
|
+
httpServer.on('upgrade', (request, socket, head) => {
|
|
34
|
+
const pathname = new URL(request.url ?? '/', 'http://localhost').pathname;
|
|
35
|
+
if (pathname !== '/codex' || !isAllowedCodexWebSocketOrigin(request, allowedOrigins)) {
|
|
36
|
+
socket.destroy();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const siteUser = authenticateSiteRequest(request);
|
|
40
|
+
if (!siteUser) {
|
|
41
|
+
socket.destroy();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
webSocketServer.handleUpgrade(request, socket, head, (webSocket) => {
|
|
46
|
+
const binding = bindCodexWebSocket({
|
|
47
|
+
socket: createCodexNodeWebSocketPort(webSocket),
|
|
48
|
+
context: { request, siteUser },
|
|
49
|
+
authorize: acquireCodexSession,
|
|
50
|
+
});
|
|
51
|
+
void binding.ready.catch((error: unknown) => {
|
|
52
|
+
console.error('Codex web session failed:', error);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
httpServer.listen(port, '127.0.0.1', () => {
|
|
58
|
+
console.log(`{{displayName}}: http://127.0.0.1:${port}`);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function authenticateSiteRequest(_request: IncomingMessage): SiteUser | null {
|
|
62
|
+
// DEMO ONLY: replace this with the website's cookie/session authentication.
|
|
63
|
+
return { id: 'local-demo-user' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function acquireCodexSession({ siteUser }: WebSessionContext) {
|
|
67
|
+
// A production host would acquire this user's isolated backend/process here.
|
|
68
|
+
return siteUser.id === 'local-demo-user' ? { surface: backend.surface } : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function shutdown(): Promise<void> {
|
|
72
|
+
for (const client of webSocketServer.clients) client.terminate();
|
|
73
|
+
await Promise.all([
|
|
74
|
+
new Promise<void>((resolve) => webSocketServer.close(() => resolve())),
|
|
75
|
+
new Promise<void>((resolve, reject) => httpServer.close((error) => error ? reject(error) : resolve())),
|
|
76
|
+
backend.close(),
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
process.once('SIGINT', () => { void shutdown(); });
|
|
81
|
+
process.once('SIGTERM', () => { void shutdown(); });
|