@corbet-labs/ccht 0.2.0 → 0.2.3
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.md +11 -6
- package/LICENSES/LGPL-3.0-linking-exception.txt +16 -0
- package/LICENSES/LGPL-3.0-only WITH LGPL-3.0-linking-exception.txt +16 -0
- package/LICENSES/dependencies/bytes-1.12.1/LICENSE +25 -0
- package/README.md +194 -212
- package/THIRD-PARTY.md +17 -0
- package/index.d.ts +6 -1
- package/index.js +8 -1
- package/package.json +17 -3
- package/source/.ci/wasm-bundle/Cargo.toml +1 -1
- package/source/CHANGELOG.md +59 -2
- package/source/Cargo.lock +9 -1
- package/source/Cargo.toml +4 -4
- package/source/LICENSE.md +11 -6
- package/source/LICENSES/LGPL-3.0-linking-exception.txt +16 -0
- package/source/LICENSES/LGPL-3.0-only WITH LGPL-3.0-linking-exception.txt +16 -0
- package/source/README.md +67 -12
- package/source/THIRD-PARTY.md +17 -0
- package/source/dependencies.tar.gz +0 -0
- package/source/src/auth.rs +435 -0
- package/source/src/configuration.rs +51 -0
- package/source/src/conversation.rs +25 -0
- package/source/src/dock.rs +564 -0
- package/source/src/lib.rs +9 -0
- package/source/src/native/client.rs +6 -0
- package/source/src/native/drivers/codex.rs +506 -0
- package/source/src/native/drivers/mod.rs +305 -0
- package/source/src/native/drivers/opencode.rs +531 -0
- package/source/src/native/env.rs +264 -0
- package/source/src/native/fixture.py +78 -1
- package/source/src/native/mod.rs +5 -0
- package/source/src/native/pool.rs +440 -0
- package/source/src/native/session.rs +6 -0
- package/source/src/native/tests.rs +204 -0
- package/source/src/transport.rs +330 -0
- package/src/auth.ts +149 -0
- package/src/components/AccountConnection.svelte +201 -0
- package/src/components/Dock.svelte +172 -0
- package/src/dock.ts +244 -0
- package/wasm/ccht_bg.wasm +0 -0
package/LICENSE.md
CHANGED
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
Copyright 2026 Julian Y. Richard Corbet.
|
|
4
4
|
|
|
5
|
-
The current ccht library is licensed under **LGPL-3.0-only
|
|
5
|
+
The current ccht library is licensed under **LGPL-3.0-only WITH
|
|
6
|
+
LGPL-3.0-linking-exception**. The complete
|
|
6
7
|
[LGPL version 3](LICENSES/LGPL-3.0-only.txt) incorporates
|
|
7
|
-
[GPL version 3](LICENSES/GPL-3.0-only.txt)
|
|
8
|
+
[GPL version 3](LICENSES/GPL-3.0-only.txt); the
|
|
9
|
+
[linking exception](<LICENSES/LGPL-3.0-only WITH LGPL-3.0-linking-exception.txt>) waives the
|
|
10
|
+
Minimal Corresponding Source, Minimal Application Code and
|
|
11
|
+
installation-information duties (LGPLv3 §§4d/4e) for combined works that
|
|
12
|
+
link statically or dynamically.
|
|
8
13
|
|
|
9
14
|
Applications may use the library under different licenses subject to the LGPL's
|
|
10
|
-
conditions. Distribution must preserve the library's notices and
|
|
11
|
-
|
|
12
|
-
The application license must not restrict those library rights.
|
|
13
|
-
|
|
15
|
+
conditions. Distribution must preserve the library's notices and the openness
|
|
16
|
+
of library modifications, including reverse engineering to debug such changes.
|
|
17
|
+
The application license must not restrict those library rights. No relinking
|
|
18
|
+
route or object files are required for combined works under the exception.
|
|
14
19
|
|
|
15
20
|
Dependencies and external agent executables retain their own licenses and service
|
|
16
21
|
terms. The library's license does not grant rights to an external runtime or
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
As a special exception to the GNU Lesser General Public License version 3
|
|
2
|
+
("LGPL3"), the copyright holders of this Library give you permission to
|
|
3
|
+
convey to a third party a Combined Work that links statically or dynamically
|
|
4
|
+
to this Library without providing any Minimal Corresponding Source or
|
|
5
|
+
Minimal Application Code as set out in 4d or providing the installation
|
|
6
|
+
information set out in section 4e, provided that you comply with the other
|
|
7
|
+
provisions of LGPL3 and provided that you meet, for the Application the
|
|
8
|
+
terms and conditions of the license(s) which apply to the Application.
|
|
9
|
+
|
|
10
|
+
Except as stated in this special exception, the provisions of LGPL3 will
|
|
11
|
+
continue to comply in full to this Library. If you modify this Library, you
|
|
12
|
+
may apply this exception to your version of this Library, but you are not
|
|
13
|
+
obliged to do so. If you do not wish to do so, delete this exception
|
|
14
|
+
statement from your version. This exception does not (and cannot) modify any
|
|
15
|
+
license terms which apply to the Application, with which you must still
|
|
16
|
+
comply.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
As a special exception to the GNU Lesser General Public License version 3
|
|
2
|
+
("LGPL3"), the copyright holders of this Library give you permission to
|
|
3
|
+
convey to a third party a Combined Work that links statically or dynamically
|
|
4
|
+
to this Library without providing any Minimal Corresponding Source or
|
|
5
|
+
Minimal Application Code as set out in 4d or providing the installation
|
|
6
|
+
information set out in section 4e, provided that you comply with the other
|
|
7
|
+
provisions of LGPL3 and provided that you meet, for the Application the
|
|
8
|
+
terms and conditions of the license(s) which apply to the Application.
|
|
9
|
+
|
|
10
|
+
Except as stated in this special exception, the provisions of LGPL3 will
|
|
11
|
+
continue to comply in full to this Library. If you modify this Library, you
|
|
12
|
+
may apply this exception to your version of this Library, but you are not
|
|
13
|
+
obliged to do so. If you do not wish to do so, delete this exception
|
|
14
|
+
statement from your version. This exception does not (and cannot) modify any
|
|
15
|
+
license terms which apply to the Application, with which you must still
|
|
16
|
+
comply.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Copyright (c) 2018 Carl Lerche
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any
|
|
4
|
+
person obtaining a copy of this software and associated
|
|
5
|
+
documentation files (the "Software"), to deal in the
|
|
6
|
+
Software without restriction, including without
|
|
7
|
+
limitation the rights to use, copy, modify, merge,
|
|
8
|
+
publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software
|
|
10
|
+
is furnished to do so, subject to the following
|
|
11
|
+
conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice
|
|
14
|
+
shall be included in all copies or substantial portions
|
|
15
|
+
of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
|
18
|
+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
|
19
|
+
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
20
|
+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
|
21
|
+
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
22
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
23
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
|
24
|
+
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
25
|
+
DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,234 +1,216 @@
|
|
|
1
|
-
# ccht
|
|
1
|
+
# @corbet-labs/ccht
|
|
2
2
|
|
|
3
|
-
Reusable conversations for
|
|
4
|
-
**ccht** keeps the conversation model in Rust and compiles
|
|
5
|
-
that same model to Wasm. Native hosts connect to installed agents using the
|
|
6
|
-
[official ACP Rust SDK](https://github.com/agentclientprotocol/rust-sdk).
|
|
3
|
+
Reusable conversations for your applications, powered by the shared Rust/Wasm model.
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
streaming, cancellation and permission requests. Your application owns its UI,
|
|
16
|
-
instructions, context, tools, user authorization, storage and HTTP/WebSocket/SSE
|
|
17
|
-
transport. No Corbet-specific prompts or database are required.
|
|
18
|
-
|
|
19
|
-
Agent reasoning, tools, authentication and protocol translation remain upstream.
|
|
20
|
-
ccht does not implement an agent loop, read provider credentials, install agents,
|
|
21
|
-
call paid model HTTP APIs or select an API-key fallback.
|
|
22
|
-
|
|
23
|
-
## Rust
|
|
24
|
-
|
|
25
|
-
```toml
|
|
26
|
-
[dependencies]
|
|
27
|
-
ccht = "0.2"
|
|
28
|
-
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
`native` is the default feature. For the portable model alone:
|
|
32
|
-
|
|
33
|
-
```toml
|
|
34
|
-
ccht = { version = "0.2", default-features = false }
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
```rust
|
|
38
|
-
use ccht::{Conversation, Event, WireEvent, acp};
|
|
39
|
-
|
|
40
|
-
let mut conversation = Conversation::new("workspace/creator");
|
|
41
|
-
conversation.apply(WireEvent::new(
|
|
42
|
-
"workspace/creator", "turn-1", 1,
|
|
43
|
-
Event::Update {
|
|
44
|
-
update: acp::SessionUpdate::AgentMessageChunk(
|
|
45
|
-
acp::ContentChunk::new("Hello".into()),
|
|
46
|
-
),
|
|
47
|
-
},
|
|
48
|
-
)).unwrap();
|
|
49
|
-
assert_eq!(conversation.turns()[0].text, "Hello");
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
[examples/native.rs](source/examples/native.rs) shows streaming from an explicitly
|
|
53
|
-
supplied installed ACP executable. It requires native agent authentication already
|
|
54
|
-
configured by the operator. Use `codex-acp`, `claude-agent-acp`, or `opencode acp`
|
|
55
|
-
as separate, reviewed runtime installations. Generic ACP executables also work.
|
|
56
|
-
No agent/runtime is bundled in the crate or browser package.
|
|
57
|
-
|
|
58
|
-
`NativeClient::connect` initializes the protocol. Create a session with its working
|
|
59
|
-
directory, explicit model (if desired) and application MCP servers. Take the event
|
|
60
|
-
receiver before prompting and consume it concurrently with the prompt future.
|
|
61
|
-
A session accepts one active prompt; another returns `Busy`. Different sessions
|
|
62
|
-
have separate streams and permission identities. History load/resume and model
|
|
63
|
-
selection require the agent's advertised capabilities; unsupported requests fail.
|
|
64
|
-
|
|
65
|
-
`session.handle().configuration()` exposes the agent's ordered model, reasoning,
|
|
66
|
-
mode and boolean controls. Call `set_model(id)` or `set_config_option(id, value)`
|
|
67
|
-
to change them. The full response replaces the advertised configuration, including
|
|
68
|
-
dependent options; configuration notifications update both the native handle and
|
|
69
|
-
the portable conversation snapshot. Unknown values fail without a fallback.
|
|
70
|
-
`SessionOptions::configuration` applies explicit settings when creating or restoring
|
|
71
|
-
a session. Retain the agent session ID and its working directory in your product
|
|
72
|
-
storage to resume completed conversations across process restarts. Never replay an
|
|
73
|
-
uncertain delivery automatically.
|
|
74
|
-
|
|
75
|
-
Permissions default to denial. Applications that opt into asking must render the
|
|
76
|
-
request, check their authority and respond with one of the advertised choices.
|
|
77
|
-
A permission callback is not a sandbox: agent-owned tools may have separate runtime
|
|
78
|
-
policy. Configure filesystem/network confinement in the host for your use case.
|
|
79
|
-
ccht advertises no client filesystem or terminal capabilities.
|
|
80
|
-
|
|
81
|
-
Cancellation asks the upstream agent to stop. If it does not stop within the
|
|
82
|
-
configured shutdown timeout, ccht closes the SDK connection, affecting **all**
|
|
83
|
-
sessions on that connection. Dropping an active prompt also closes its connection.
|
|
84
|
-
Use one connection per independent workload when cancellation must be isolated.
|
|
85
|
-
Bounded event channels fail explicitly on an unresponsive consumer.
|
|
86
|
-
|
|
87
|
-
`AgentCommand::with_working_directory` sets the actual agent startup directory
|
|
88
|
-
using installed Linux/macOS `env` launchers. Windows currently reports this helper
|
|
89
|
-
as unsupported because the SDK launch configuration lacks a startup-directory
|
|
90
|
-
field. Session cwd alone does not constrain files read during agent startup.
|
|
91
|
-
Linux is covered by process-level tests; macOS support is source-reviewed.
|
|
92
|
-
|
|
93
|
-
ACP enables `serde_json`'s `preserve_order` feature through Cargo feature
|
|
94
|
-
unification. Applications that hash or sign JSON must explicitly canonicalize
|
|
95
|
-
object keys; depending on a `Value` map's incidental iteration order is unsafe.
|
|
96
|
-
|
|
97
|
-
## Browser
|
|
98
|
-
|
|
99
|
-
```sh
|
|
100
|
-
npm install @corbet-labs/ccht
|
|
101
|
-
```
|
|
5
|
+
The root entry (`createConversation`) owns the conversation state machine. The
|
|
6
|
+
`./auth` entry adds typed auth/challenge models mirroring `ccht::auth`, the
|
|
7
|
+
`./dock` entry adds framework-free dock state, and
|
|
8
|
+
`./components/AccountConnection.svelte` adds the first shared Svelte 5 account
|
|
9
|
+
connector alongside the generic `./components/Dock.svelte` edge rail. All
|
|
10
|
+
entries are product-neutral: no product names, prompts, or
|
|
11
|
+
pricing appear in this package.
|
|
102
12
|
|
|
103
13
|
```js
|
|
104
14
|
import { createConversation } from '@corbet-labs/ccht';
|
|
105
|
-
|
|
106
|
-
import
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// After your authenticated application transport delivers a ccht WireEvent:
|
|
110
|
-
const snapshot = conversation.applyEvent(event);
|
|
111
|
-
render(snapshot.turns);
|
|
112
|
-
// When the view is destroyed:
|
|
113
|
-
conversation.free();
|
|
15
|
+
import { isAuthenticated, validateChallenge } from '@corbet-labs/ccht/auth';
|
|
16
|
+
import { createDockManager, validateDockId } from '@corbet-labs/ccht/dock';
|
|
17
|
+
import AccountConnection from '@corbet-labs/ccht/components/AccountConnection.svelte';
|
|
18
|
+
import Dock from '@corbet-labs/ccht/components/Dock.svelte';
|
|
114
19
|
```
|
|
115
20
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
application server or desktop host runs `ccht::native`. The package makes no
|
|
119
|
-
network connection apart from loading its Wasm module. The host decides which
|
|
120
|
-
backend is available and how users authenticate to the application.
|
|
21
|
+
The Svelte component ships as source and needs `svelte` `^5` (a peer
|
|
22
|
+
dependency). It has no other dependencies and imports no CSS framework.
|
|
121
23
|
|
|
122
|
-
##
|
|
24
|
+
## What is shared vs what the app owns
|
|
123
25
|
|
|
124
|
-
|
|
26
|
+
Shared (this package):
|
|
125
27
|
|
|
126
|
-
|
|
127
|
-
|
|
28
|
+
- Conversation state and ordered event replay (Wasm `ConversationModel`).
|
|
29
|
+
- Auth/challenge types and validators (`AuthState`, `Challenge`,
|
|
30
|
+
`CredentialsProvider`).
|
|
31
|
+
- Presentational account connector markup with stable styling hooks.
|
|
128
32
|
|
|
129
|
-
|
|
130
|
-
wasm: new URL('https://jsr.io/@corbet-labs/ccht/0.2.0/wasm/ccht_bg.wasm'),
|
|
131
|
-
});
|
|
132
|
-
```
|
|
33
|
+
Owned by the embedding application:
|
|
133
34
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
35
|
+
- Transport: delivering `WireEvent`s (HTTP/WebSocket/SSE), serving the Wasm
|
|
36
|
+
bytes, and polling login operations until they complete.
|
|
37
|
+
- Effects: spawning agents, starting or cancelling logins, signing out. These
|
|
38
|
+
never run in browser library code; the component drives them through
|
|
39
|
+
app-supplied async callbacks and the app backend does the work.
|
|
40
|
+
- Storage: where credentials persist (OS keychain natively, origin-scoped
|
|
41
|
+
browser storage on the web). The library defines the interface only.
|
|
42
|
+
- Product copy: account labels, help text, links, and error wording beyond the
|
|
43
|
+
component's generic strings.
|
|
138
44
|
|
|
139
|
-
|
|
45
|
+
Rule: browser code in this package never spawns agents, performs network I/O,
|
|
46
|
+
or holds ambient authority. Service keys are explicit strings passed by the
|
|
47
|
+
app, and secrets are opaque bytes that are never logged.
|
|
140
48
|
|
|
141
|
-
|
|
49
|
+
## Auth entry (`@corbet-labs/ccht/auth`)
|
|
142
50
|
|
|
143
|
-
|
|
144
|
-
python -m pip install ccht==0.2.0
|
|
145
|
-
```
|
|
51
|
+
Plain types and functions, no DOM access.
|
|
146
52
|
|
|
147
|
-
|
|
148
|
-
from ccht import Conversation
|
|
53
|
+
`AuthState` mirrors the Rust `snake_case` JSON byte-for-byte:
|
|
149
54
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
55
|
+
| Rust | JSON | TypeScript |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| `Unknown` | `"unknown"` | `'unknown'` |
|
|
58
|
+
| `Unauthenticated` | `"unauthenticated"` | `'unauthenticated'` |
|
|
59
|
+
| `Authenticated { account }` | `{"authenticated": {"account": "…"}}` | `{ authenticated: { account?: string \| null } }` |
|
|
155
60
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
Assign identities and sequences on a trusted application host. Never let a browser
|
|
169
|
-
select another user's conversation without server-side authorization. Sequence
|
|
170
|
-
numbers are **not** SSE cursors or database row IDs. The app stores its own event
|
|
171
|
-
log and replays each request in order. Already-applied sequences are ignored;
|
|
172
|
-
gaps, a different conversation, malformed events and new events after completion
|
|
173
|
-
fail without mutating state. Delivery identity must not be reused for another turn.
|
|
174
|
-
|
|
175
|
-
A model holds at most 256 turns, 8 MiB per turn and 1 MiB per event. Applications
|
|
176
|
-
should page long histories into separate models and limit streamed inputs before
|
|
177
|
-
parsing them. Durable transcript storage and reconnection are application concerns;
|
|
178
|
-
a serialized snapshot is a render result, not a native-agent session checkpoint.
|
|
179
|
-
|
|
180
|
-
Namer is the first integration: Creator/Critic have independent conversation
|
|
181
|
-
scopes, durable application events, authenticated SSE and cancellation. Its queue
|
|
182
|
-
uses a fresh native session per turn with bounded role history supplied by Namer.
|
|
183
|
-
CareerVector's TUI reuses the native session API while keeping CV tools, prompts
|
|
184
|
-
and revision checks in its own code. Explicit local inference can emit the same
|
|
185
|
-
`WireEvent` contract through an application-owned backend.
|
|
186
|
-
|
|
187
|
-
## Building the Wasm package
|
|
188
|
-
|
|
189
|
-
Use Rust with `wasm32-unknown-unknown` installed. The checked-in generator uses the
|
|
190
|
-
same exact `wasm-bindgen` version as the Rust crate; no global Wasm CLI is required.
|
|
191
|
-
|
|
192
|
-
```sh
|
|
193
|
-
cargo build --locked --release --no-default-features --features web --target wasm32-unknown-unknown
|
|
194
|
-
cargo run --locked --release -p ccht-wasm-bundle -- target/wasm32-unknown-unknown/release/ccht.wasm web/wasm
|
|
195
|
-
```
|
|
61
|
+
Helpers: `isAuthenticated(state)` mirrors `AuthState::authenticated`,
|
|
62
|
+
`authAccount(state)` returns the display-only label (or null), and
|
|
63
|
+
`parseAuthState(value)` / `isAuthState(value)` validate decoded JSON. The
|
|
64
|
+
label is provenance for the UI, never an authentication proof.
|
|
65
|
+
|
|
66
|
+
`Challenge` is `{ verification_url, user_code }`. The backend issues it, pins
|
|
67
|
+
any provider-specific endpoint, and polls for completion; the browser only
|
|
68
|
+
renders a validated challenge. `validateChallenge(value)` accepts an `https:`
|
|
69
|
+
URL with a host (no embedded credentials) and a non-empty code of up to 64
|
|
70
|
+
ASCII alphanumeric or `-` characters. It never throws, so callers can gate
|
|
71
|
+
rendering on it. The component validates internally and renders waiting text
|
|
72
|
+
for a missing or malformed challenge instead of an unvalidated link.
|
|
196
73
|
|
|
197
|
-
|
|
198
|
-
under `source/`, original notices, and the full locked dependency sources in
|
|
199
|
-
`source/dependencies.tar.gz`. Cargo's vendor checksums are preserved inside that
|
|
200
|
-
archive. To rebuild from the distributed package, extract it inside `source/`:
|
|
74
|
+
`CredentialsProvider` mirrors the Rust trait with JS method names:
|
|
201
75
|
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
76
|
+
```ts
|
|
77
|
+
interface CredentialsProvider {
|
|
78
|
+
readCredentials(service: string): Promise<Credential | null>;
|
|
79
|
+
writeCredentials(service: string, credential: Credential): Promise<void>;
|
|
80
|
+
deleteCredentials(service: string): Promise<void>;
|
|
81
|
+
}
|
|
207
82
|
```
|
|
208
83
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
84
|
+
`Credential` is `{ account: string; secret: Uint8Array }`. Encode at the app
|
|
85
|
+
boundary (`TextEncoder`/base64) when the store needs text. Service keys are
|
|
86
|
+
app-defined (for example `assistant-login:<provider>`); deleting a missing
|
|
87
|
+
entry succeeds, and a rejected promise signals store failure. There is no
|
|
88
|
+
environment-variable override on the web: the store alone decides, matching
|
|
89
|
+
the documented `ApiKeyState` behavior on Wasm.
|
|
90
|
+
|
|
91
|
+
## Component (`AccountConnection.svelte`)
|
|
92
|
+
|
|
93
|
+
Svelte 5 runes. Props:
|
|
94
|
+
|
|
95
|
+
| Prop | Type | Meaning |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| `account` | `{ id: string; label: string; kind: 'chatgpt' \| 'opencode_go' \| string }` | Product-neutral identity. Known kinds select the ceremony; unknown kinds fall back to a generic sign-in button. |
|
|
98
|
+
| `state` | `AuthState` | Observed login state (defaults to `'unknown'`). |
|
|
99
|
+
| `busy` | `boolean` | An app-owned operation is in flight; actions disable, cancel shows. |
|
|
100
|
+
| `challenge` | `Challenge \| null` (optional) | Device-code challenge supplied by the app after `onStart` starts polling. |
|
|
101
|
+
| `onStart` | `() => void \| Promise<void>` | Begin the login ceremony (for example start a device flow). |
|
|
102
|
+
| `onCancel` | `() => void \| Promise<void>` | Abort the in-flight operation; also backs Disconnect when `onDisconnect` is omitted. |
|
|
103
|
+
| `onKeySubmit` | `(key: string) => void \| Promise<void>` | Persist a trimmed account key (minimum 8 characters). |
|
|
104
|
+
| `onDisconnect` | optional | Sign out when authenticated. Falls back to `onCancel`, which can branch on app state. |
|
|
105
|
+
|
|
106
|
+
Rendering:
|
|
107
|
+
|
|
108
|
+
- Unauthenticated `chatgpt`: sign-in button, then waiting text while `busy`,
|
|
109
|
+
then the device-code view (code, copy button, sign-in link) once a valid
|
|
110
|
+
`challenge` arrives. The copy button opens the link during the click (so
|
|
111
|
+
popup blockers do not trigger) and falls back to a manual-copy message when
|
|
112
|
+
the clipboard API is unavailable.
|
|
113
|
+
- Unauthenticated `opencode_go`: password key form that clears on submit.
|
|
114
|
+
- Authenticated: connection status with the account label (when known) and a
|
|
115
|
+
Disconnect button.
|
|
116
|
+
- `unknown` state: neutral checking text with no ceremony buttons.
|
|
117
|
+
|
|
118
|
+
Callbacks are fire-and-forget from the component's view: the app must handle
|
|
119
|
+
its own failures and surface them through its own operation state, which it
|
|
120
|
+
renders outside the component. Callback rejections are therefore ignored here
|
|
121
|
+
by design.
|
|
122
|
+
|
|
123
|
+
Accessibility: semantic `section`/`form`/`label`/`button` elements, an
|
|
124
|
+
`aria-label` naming the account connection, `role="status"` for status lines,
|
|
125
|
+
an `aria-live="polite"` region announcing challenge arrival, and
|
|
126
|
+
`role="alert"` for the clipboard fallback. The key input is labelled,
|
|
127
|
+
password-masked, and non-persistent.
|
|
128
|
+
|
|
129
|
+
## Dock entry (`@corbet-labs/ccht/dock`) and component (`Dock.svelte`)
|
|
130
|
+
|
|
131
|
+
Framework-free dock state plus a generic Svelte 5 edge rail. `DockKind`
|
|
132
|
+
(`'chat' | 'config' | 'custom'`) classifies a dock and `Placement`
|
|
133
|
+
(`'left' | 'right' | 'bottom' | 'inline'`) places it. The state module is
|
|
134
|
+
plain TypeScript with no DOM access.
|
|
135
|
+
|
|
136
|
+
`validateDockId(id)` returns an error message or null (never throws). Rules:
|
|
137
|
+
a non-empty string of at most 64 ASCII `[a-z0-9-_]` characters.
|
|
138
|
+
|
|
139
|
+
`createDockManager()` returns an independent manager:
|
|
140
|
+
|
|
141
|
+
| Method | Meaning |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| `register(id, kind, placement)` | Register a closed dock. Throws the validation message for a malformed id, `DuplicateDock: <id>` for a repeat. |
|
|
144
|
+
| `open(id)` / `close(id)` | Mark a dock open or closed. Unknown ids throw `UnknownDock: <id>`. |
|
|
145
|
+
| `openWithFocus(id, token)` | Open a dock and stage an opaque focus token (non-empty string) for the app to consume. |
|
|
146
|
+
| `toggle(id)` | Flip a dock's open state and return the new state. |
|
|
147
|
+
| `isOpen(id)` | Whether a dock is open. |
|
|
148
|
+
| `placement(id)` / `setPlacement(id, placement)` | Read or move a dock's placement. Invalid placements throw `invalid dock placement`. |
|
|
149
|
+
| `openDocks()` | Ids of open docks, in registration order. |
|
|
150
|
+
| `takeFocusToken()` | Take the staged focus token once, clearing it; null when none is staged. |
|
|
151
|
+
| `closeAll()` | Mark every dock closed. |
|
|
152
|
+
| `serialize()` | Persist `[{id, kind, placement, open}]` as JSON, in registration order. The focus token is never persisted. |
|
|
153
|
+
| `restore(json)` | Replace all state from `serialize` output. Every entry is validated first, so a malformed snapshot (thrown as `invalid dock snapshot: …`) leaves current state untouched, and any staged focus token is cleared. |
|
|
154
|
+
|
|
155
|
+
`Dock.svelte` renders one named dock as an edge rail for the `left` and
|
|
156
|
+
`right` placements. Svelte 5 runes. Props:
|
|
157
|
+
|
|
158
|
+
| Prop | Type | Meaning |
|
|
159
|
+
| --- | --- | --- |
|
|
160
|
+
| `side` | `'left' \| 'right'` | Which window edge the rail pins to. |
|
|
161
|
+
| `title` | `string` | Panel heading and dialog label. |
|
|
162
|
+
| `open` | `boolean` | App-owned visibility state. |
|
|
163
|
+
| `onClose` | `() => void` | Close intent (tab is hidden while open; Esc key also closes). |
|
|
164
|
+
| `onOpen` | `() => void` | Open intent from the edge tab button. |
|
|
165
|
+
| `tabLabel` | `string` | Short tab caption. |
|
|
166
|
+
| `tabSummary` | `string` (optional, defaults to `''`) | Detail shown in the tab button and panel head. |
|
|
167
|
+
| `closeLabel` | optional `string` | Close button label (defaults to `Close {title}`). |
|
|
168
|
+
| `panelId` | `string` | Panel element id, referenced by the tab's `aria-controls`. |
|
|
169
|
+
| `children` | `Snippet` | Panel body content. |
|
|
170
|
+
|
|
171
|
+
Rendering:
|
|
172
|
+
|
|
173
|
+
- Closed: a fixed edge tab button (`aria-expanded="false"`,
|
|
174
|
+
`aria-controls={panelId}`, labelled `Open {title}: {tabSummary}` or
|
|
175
|
+
`Open {title}`). The panel stays mounted but hidden and inert.
|
|
176
|
+
- Open: a `role="dialog"` panel with a head (`h2` title, summary paragraph,
|
|
177
|
+
Close button) and a body rendering `children`.
|
|
178
|
+
- Opening moves focus to the first visible, enabled control in the panel
|
|
179
|
+
(or the panel itself); closing returns focus to the tab or the previously
|
|
180
|
+
focused element. Esc closes.
|
|
181
|
+
- The `bottom` and `inline` placements are state-only: they persist and
|
|
182
|
+
restore through the manager but have no rail chrome here. Custom hosts
|
|
183
|
+
(for example a ratatui overlay or an inline sheet) render them.
|
|
184
|
+
|
|
185
|
+
Stable classes: `ccht-dock-tab`, `ccht-dock-tab-text`, `ccht-dock-panel`,
|
|
186
|
+
`ccht-dock-head`, `ccht-dock-title`, `ccht-dock-close`, `ccht-dock-body`.
|
|
187
|
+
The tab and panel carry `data-side` (`left` / `right`) and the panel carries
|
|
188
|
+
`data-open` (`true` / `false`).
|
|
189
|
+
|
|
190
|
+
CSS variables (with fallbacks when unset): `--ccht-fg`, `--ccht-muted`,
|
|
191
|
+
`--ccht-accent`, `--ccht-border`, `--ccht-panel-bg`, `--ccht-tab-bg`,
|
|
192
|
+
`--ccht-tab-bg-hover`, `--ccht-fg-bright`.
|
|
193
|
+
|
|
194
|
+
Rule: the app owns dock effects. The manager never persists, the component
|
|
195
|
+
never fetches, spawns, or stores; visibility, persistence, and focus targets
|
|
196
|
+
beyond the panel run through app callbacks and app-held state.
|
|
197
|
+
|
|
198
|
+
## Styling hooks
|
|
199
|
+
|
|
200
|
+
Unstyled-but-hooked markup: layout comes from the app, colors resolve through
|
|
201
|
+
CSS custom properties with plain fallbacks. Stable classes:
|
|
202
|
+
|
|
203
|
+
`ccht-account`, `ccht-status`, `ccht-hint`, `ccht-error`, `ccht-challenge`,
|
|
204
|
+
`ccht-code`, `ccht-button`, `ccht-cancel`, `ccht-disconnect`, `ccht-link`,
|
|
205
|
+
`ccht-keyform`, `ccht-label`, `ccht-input`.
|
|
206
|
+
|
|
207
|
+
CSS variables (with fallbacks when unset): `--ccht-fg`, `--ccht-muted`,
|
|
208
|
+
`--ccht-error`, `--ccht-accent`, `--ccht-code-bg`. Example:
|
|
209
|
+
|
|
210
|
+
```css
|
|
211
|
+
:root {
|
|
212
|
+
--ccht-muted: #a5b5cb;
|
|
213
|
+
--ccht-error: #fda4af;
|
|
214
|
+
--ccht-accent: #7bb8ff;
|
|
215
|
+
}
|
|
216
|
+
```
|
package/THIRD-PARTY.md
CHANGED
|
@@ -21,8 +21,25 @@ comply with their runtime and service conditions when packaging them yourself.
|
|
|
21
21
|
Do not infer permission to offer a customer-facing subscription login or pooled
|
|
22
22
|
account service from the wrapper's permissive license.
|
|
23
23
|
|
|
24
|
+
Permissive references informing the native login drivers (no vendored code;
|
|
25
|
+
hand-rolled protocol shapes with attribution in the module docs):
|
|
26
|
+
|
|
27
|
+
- Codex app-server protocol (`account/login/start` device-code challenge,
|
|
28
|
+
`account/login/completed`, `account/read`) from openai/codex under
|
|
29
|
+
Apache-2.0 informs `src/native/drivers/codex.rs` (`CodexDeviceDriver`).
|
|
30
|
+
The driver spawns the caller's Codex `app-server`, relays the
|
|
31
|
+
host-allowlisted challenge, and projects presence only.
|
|
32
|
+
- OpenCode `serve` control plane (`GET /global/health`,
|
|
33
|
+
`PUT /auth/opencode-go`, `GET /provider`) under MIT informs
|
|
34
|
+
`src/native/drivers/opencode.rs` (`OpenCodeKeyDriver`). The driver uses
|
|
35
|
+
loopback `opencode serve` with an ephemeral port, an env-only server
|
|
36
|
+
password, and hand-rolled HTTP with no new dependencies.
|
|
37
|
+
|
|
38
|
+
Preserve their original grants and notices when reusing these shapes.
|
|
39
|
+
|
|
24
40
|
Upstream sources: [ACP SDK](https://github.com/agentclientprotocol/rust-sdk),
|
|
25
41
|
[Codex ACP](https://github.com/agentclientprotocol/codex-acp),
|
|
42
|
+
[openai/codex](https://github.com/openai/codex),
|
|
26
43
|
[Claude ACP](https://github.com/agentclientprotocol/claude-agent-acp),
|
|
27
44
|
[OpenCode](https://github.com/anomalyco/opencode),
|
|
28
45
|
[wasm-bindgen](https://github.com/wasm-bindgen/wasm-bindgen).
|
package/index.d.ts
CHANGED
|
@@ -51,7 +51,12 @@ export interface Conversation {
|
|
|
51
51
|
snapshot(): ConversationSnapshot;
|
|
52
52
|
free(): void;
|
|
53
53
|
}
|
|
54
|
-
/** Transport/authentication and persistence belong to the embedding application.
|
|
54
|
+
/** Transport/authentication and persistence belong to the embedding application.
|
|
55
|
+
*
|
|
56
|
+
* Only the first call's `wasm` option initializes the shared module; later
|
|
57
|
+
* calls reuse it. `applyEvent` returns the fresh snapshot and throws on
|
|
58
|
+
* invalid, out-of-scope, gapped, terminal, or freed input. `free` is idempotent.
|
|
59
|
+
*/
|
|
55
60
|
export function createConversation(conversationId: string, options?: {
|
|
56
61
|
wasm?: URL | string | Request | Response | BufferSource | WebAssembly.Module;
|
|
57
62
|
}): Promise<Conversation>;
|
package/index.js
CHANGED
|
@@ -3,7 +3,14 @@ import initialize, { ConversationModel } from './wasm/ccht.js';
|
|
|
3
3
|
|
|
4
4
|
let initialization;
|
|
5
5
|
|
|
6
|
-
/** Initialize the shared Rust/Wasm model. Supply an explicit Wasm URL when bundling.
|
|
6
|
+
/** Initialize the shared Rust/Wasm model. Supply an explicit Wasm URL when bundling.
|
|
7
|
+
*
|
|
8
|
+
* Only the first call's `wasm` option initializes the shared module; later
|
|
9
|
+
* calls reuse it. The returned `applyEvent` applies one ordered event and
|
|
10
|
+
* returns the fresh snapshot (the underlying Rust boolean for "changed" is
|
|
11
|
+
* not exposed); it throws on invalid JSON, scope, gap, terminal, or freed
|
|
12
|
+
* state. `snapshot` returns the parsed render state and `free` is idempotent.
|
|
13
|
+
*/
|
|
7
14
|
export async function createConversation(conversationId, options = {}) {
|
|
8
15
|
if (!initialization) {
|
|
9
16
|
initialization = initialize(options.wasm === undefined ? undefined : { module_or_path: options.wasm })
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@corbet-labs/ccht",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Reusable conversations for your applications, powered by the shared Rust/Wasm model",
|
|
5
|
-
"license": "LGPL-3.0-only",
|
|
5
|
+
"license": "LGPL-3.0-only WITH LGPL-3.0-linking-exception",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./index.js",
|
|
8
8
|
"types": "./index.d.ts",
|
|
@@ -11,11 +11,22 @@
|
|
|
11
11
|
"types": "./index.d.ts",
|
|
12
12
|
"import": "./index.js"
|
|
13
13
|
},
|
|
14
|
+
"./auth": {
|
|
15
|
+
"types": "./src/auth.ts",
|
|
16
|
+
"import": "./src/auth.ts"
|
|
17
|
+
},
|
|
18
|
+
"./dock": {
|
|
19
|
+
"types": "./src/dock.ts",
|
|
20
|
+
"import": "./src/dock.ts"
|
|
21
|
+
},
|
|
22
|
+
"./components/AccountConnection.svelte": "./src/components/AccountConnection.svelte",
|
|
23
|
+
"./components/Dock.svelte": "./src/components/Dock.svelte",
|
|
14
24
|
"./ccht_bg.wasm": "./wasm/ccht_bg.wasm"
|
|
15
25
|
},
|
|
16
26
|
"files": [
|
|
17
27
|
"index.js",
|
|
18
28
|
"index.d.ts",
|
|
29
|
+
"src/",
|
|
19
30
|
"wasm/",
|
|
20
31
|
"source/",
|
|
21
32
|
"LICENSE",
|
|
@@ -31,6 +42,9 @@
|
|
|
31
42
|
"publishConfig": {
|
|
32
43
|
"access": "public"
|
|
33
44
|
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"svelte": "^5"
|
|
47
|
+
},
|
|
34
48
|
"sideEffects": false,
|
|
35
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "baa0b02cd88ad171d8c9a507a1d08c19d51bcc55"
|
|
36
50
|
}
|