@tangleai/assistant 0.21.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/CHANGELOG.md +35 -0
- package/LICENSE +21 -0
- package/README.md +41 -0
- package/package.json +56 -0
- package/src/actions.d.ts +237 -0
- package/src/actions.js +145 -0
- package/src/component.d.ts +55 -0
- package/src/component.js +93 -0
- package/src/controller.d.ts +36 -0
- package/src/controller.js +322 -0
- package/src/index.d.ts +4 -0
- package/src/index.js +4 -0
- package/src/settings.d.ts +6 -0
- package/src/settings.js +11 -0
- package/src/state.d.ts +32 -0
- package/src/state.js +40 -0
- package/src/viewmodel.d.ts +55 -0
- package/src/viewmodel.js +77 -0
- package/src/views.d.ts +332 -0
- package/src/views.js +208 -0
- package/styles/assistant.css +115 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @tangleai/assistant
|
|
2
|
+
|
|
3
|
+
## 0.21.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Update the exact Jaren foundation dependencies and source pin to the published
|
|
8
|
+
0.84.3 release after verifying its AI-free archives against source-built bytes.
|
|
9
|
+
Retain Tangle's model, context and agent ownership and align the development
|
|
10
|
+
Node pin with 24.20.0. Tangle publication remains a manual author action.
|
|
11
|
+
Allow release preparation after an already committed local release while
|
|
12
|
+
preserving its record and rejecting unprepared version edits.
|
|
13
|
+
- Updated dependencies
|
|
14
|
+
- @tangleai/models@0.21.1
|
|
15
|
+
- @tangleai/context@0.21.1
|
|
16
|
+
- @tangleai/agents@0.21.1
|
|
17
|
+
|
|
18
|
+
## 0.21.0
|
|
19
|
+
|
|
20
|
+
### Minor Changes
|
|
21
|
+
|
|
22
|
+
- Receive the reusable assistant and injected slot ledger storage, preserving
|
|
23
|
+
streaming, transcript and evidenced-memory behavior with owned cleanup.
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- Updated dependencies
|
|
28
|
+
- Updated dependencies
|
|
29
|
+
- @tangleai/models@0.21.0
|
|
30
|
+
- @tangleai/context@0.21.0
|
|
31
|
+
- @tangleai/agents@0.21.0
|
|
32
|
+
|
|
33
|
+
## Unreleased
|
|
34
|
+
|
|
35
|
+
Received the existing assistant with injected host services and owned lifecycle.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joham (jklarenbeek@gmail.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @tangleai/assistant
|
|
2
|
+
|
|
3
|
+
Reusable assistant state, actions and request lifecycle. The headless root imports
|
|
4
|
+
no renderer or DOM. Hosts inject their toolbox, system instructions, state access,
|
|
5
|
+
settings/transcript stores and ledger. Dispose aborts requests and releases the
|
|
6
|
+
optional owned storage; cancel prevents late stream results from reappearing.
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
import { mountAssistant } from '@tangleai/assistant/component';
|
|
10
|
+
import '@tangleai/assistant/styles/assistant.css';
|
|
11
|
+
const assistant = mountAssistant(document.querySelector('#assistant'), {
|
|
12
|
+
title: 'My workspace', capabilities: ['Inspect a document'], toolbox,
|
|
13
|
+
system: 'Help with the tools this workspace provides.',
|
|
14
|
+
aiStorage: settingsSlot, aiChat: transcriptSlot, ledger,
|
|
15
|
+
onDispose: () => ownedLedgerStorage.close(),
|
|
16
|
+
});
|
|
17
|
+
assistant.update({ title: 'Review workspace' }); // retains the draft and request
|
|
18
|
+
await assistant.dispose();
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`mountAssistant(node, options)` returns `read`, `dispatch`, `subscribe`,
|
|
22
|
+
`update` and asynchronous, idempotent `dispose`, plus its headless controller.
|
|
23
|
+
`createAssistantWidget(options)` supplies app widget mount/update/unmount hooks.
|
|
24
|
+
Presentation options include title, launcher text, intro, capabilities and
|
|
25
|
+
settings hint. The host supplies its toolbox, system prompt, provider fetch and
|
|
26
|
+
settings/transcript slots; omitted slots and ledger are in memory. Slot writes
|
|
27
|
+
must throw or return false on failure. An injected ledger remains host-owned
|
|
28
|
+
unless `onDispose` explicitly closes it. Never put provider settings in a data
|
|
29
|
+
export. See [browser data transfer](../../docs/migrations/jaren-ai/BROWSER_DATA.md).
|
|
30
|
+
|
|
31
|
+
The root exports state/actions/controller without rendering dependencies.
|
|
32
|
+
`/component` imports the Jaren app, Markdown and Mermaid renderers. CSS selectors
|
|
33
|
+
are scoped to `.tangle-assistant`; host tokens have fallbacks. Each mount owns
|
|
34
|
+
its datalist IDs, draft, stream and fragment links. Cancelling or disposing
|
|
35
|
+
aborts current requests and ignores late results from non-cooperating providers.
|
|
36
|
+
Disposed controls cannot dispatch into the host. Presentation updates retain
|
|
37
|
+
input nodes and do not reset selection or the active request.
|
|
38
|
+
|
|
39
|
+
The controller exposes its current request `signal`. Hosts can combine it with
|
|
40
|
+
their own lifetime signal for child authoring requests. Cancellation fences
|
|
41
|
+
later tool calls and Pages refuses late authoring results before publication.
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tangleai/assistant",
|
|
3
|
+
"version": "0.21.1",
|
|
4
|
+
"description": "Reusable assistant state, streaming lifecycle and optional Jaren visual component.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./src/index.js",
|
|
8
|
+
"types": "./src/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./src/index.d.ts",
|
|
12
|
+
"import": "./src/index.js",
|
|
13
|
+
"default": "./src/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json",
|
|
16
|
+
"./component": {
|
|
17
|
+
"types": "./src/component.d.ts",
|
|
18
|
+
"import": "./src/component.js",
|
|
19
|
+
"default": "./src/component.js"
|
|
20
|
+
},
|
|
21
|
+
"./styles/assistant.css": "./styles/assistant.css"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=24"
|
|
25
|
+
},
|
|
26
|
+
"sideEffects": [
|
|
27
|
+
"**/*.css"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@tangleai/models": "^0.21.1",
|
|
31
|
+
"@tangleai/context": "^0.21.1",
|
|
32
|
+
"@tangleai/agents": "^0.21.1",
|
|
33
|
+
"@jarenjs/json": "0.84.3",
|
|
34
|
+
"@jarenjs/app": "0.84.3",
|
|
35
|
+
"@jarenjs/md": "0.84.3",
|
|
36
|
+
"@jarenjs/mermaid": "0.84.3"
|
|
37
|
+
},
|
|
38
|
+
"private": false,
|
|
39
|
+
"files": [
|
|
40
|
+
"src/**/*.js",
|
|
41
|
+
"src/**/*.d.ts",
|
|
42
|
+
"README.md",
|
|
43
|
+
"LICENSE",
|
|
44
|
+
"CHANGELOG.md",
|
|
45
|
+
"styles/**/*.css"
|
|
46
|
+
],
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public",
|
|
49
|
+
"registry": "https://registry.npmjs.org/"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/jklarenbeek/tangleai.git",
|
|
54
|
+
"directory": "components/assistant"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/actions.d.ts
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/** Assistant transitions received from the existing panel. */
|
|
2
|
+
export const ASSISTANT_ACTIONS: {
|
|
3
|
+
'ai/cancel': {
|
|
4
|
+
patch: {
|
|
5
|
+
op: string;
|
|
6
|
+
path: string;
|
|
7
|
+
value: string;
|
|
8
|
+
}[];
|
|
9
|
+
effects: {
|
|
10
|
+
run: string;
|
|
11
|
+
}[];
|
|
12
|
+
};
|
|
13
|
+
'ai/toggle': {
|
|
14
|
+
patch: {
|
|
15
|
+
op: string;
|
|
16
|
+
path: string;
|
|
17
|
+
value: {
|
|
18
|
+
$not: string;
|
|
19
|
+
};
|
|
20
|
+
}[];
|
|
21
|
+
effects: {
|
|
22
|
+
run: string;
|
|
23
|
+
}[];
|
|
24
|
+
};
|
|
25
|
+
'ai/settings-toggle': {
|
|
26
|
+
patch: {
|
|
27
|
+
op: string;
|
|
28
|
+
path: string;
|
|
29
|
+
value: {
|
|
30
|
+
$not: string;
|
|
31
|
+
};
|
|
32
|
+
}[];
|
|
33
|
+
};
|
|
34
|
+
'ai/settings-open': {
|
|
35
|
+
patch: {
|
|
36
|
+
op: string;
|
|
37
|
+
path: string;
|
|
38
|
+
value: string;
|
|
39
|
+
}[];
|
|
40
|
+
};
|
|
41
|
+
'ai/setting': {
|
|
42
|
+
patch: ({
|
|
43
|
+
op: string;
|
|
44
|
+
path: {
|
|
45
|
+
$concat: string[];
|
|
46
|
+
};
|
|
47
|
+
value: string;
|
|
48
|
+
} | {
|
|
49
|
+
op: string;
|
|
50
|
+
path: string;
|
|
51
|
+
value: {
|
|
52
|
+
status: string;
|
|
53
|
+
detail: any;
|
|
54
|
+
models: any[];
|
|
55
|
+
};
|
|
56
|
+
})[];
|
|
57
|
+
};
|
|
58
|
+
'ai/probe': {
|
|
59
|
+
patch: {
|
|
60
|
+
op: string;
|
|
61
|
+
path: string;
|
|
62
|
+
value: {
|
|
63
|
+
status: string;
|
|
64
|
+
detail: any;
|
|
65
|
+
models: any[];
|
|
66
|
+
};
|
|
67
|
+
}[];
|
|
68
|
+
effects: {
|
|
69
|
+
run: string;
|
|
70
|
+
}[];
|
|
71
|
+
};
|
|
72
|
+
'ai/probe-result': {
|
|
73
|
+
patch: {
|
|
74
|
+
op: string;
|
|
75
|
+
path: string;
|
|
76
|
+
value: string;
|
|
77
|
+
}[];
|
|
78
|
+
};
|
|
79
|
+
'ai/save-settings': {
|
|
80
|
+
patch: {
|
|
81
|
+
op: string;
|
|
82
|
+
path: string;
|
|
83
|
+
value: boolean;
|
|
84
|
+
}[];
|
|
85
|
+
effects: {
|
|
86
|
+
run: string;
|
|
87
|
+
}[];
|
|
88
|
+
};
|
|
89
|
+
'ai/draft': {
|
|
90
|
+
patch: {
|
|
91
|
+
op: string;
|
|
92
|
+
path: string;
|
|
93
|
+
value: string;
|
|
94
|
+
}[];
|
|
95
|
+
};
|
|
96
|
+
'ai/send': {
|
|
97
|
+
effects: {
|
|
98
|
+
run: string;
|
|
99
|
+
}[];
|
|
100
|
+
};
|
|
101
|
+
'ai/user': {
|
|
102
|
+
patch: ({
|
|
103
|
+
op: string;
|
|
104
|
+
path: string;
|
|
105
|
+
value: {
|
|
106
|
+
role: string;
|
|
107
|
+
content: string;
|
|
108
|
+
};
|
|
109
|
+
} | {
|
|
110
|
+
op: string;
|
|
111
|
+
path: string;
|
|
112
|
+
value: string;
|
|
113
|
+
} | {
|
|
114
|
+
op: string;
|
|
115
|
+
path: string;
|
|
116
|
+
value: number;
|
|
117
|
+
})[];
|
|
118
|
+
effects: {
|
|
119
|
+
run: string;
|
|
120
|
+
}[];
|
|
121
|
+
};
|
|
122
|
+
'ai/delta': {
|
|
123
|
+
patch: {
|
|
124
|
+
op: string;
|
|
125
|
+
path: string;
|
|
126
|
+
value: {
|
|
127
|
+
$concat: string[];
|
|
128
|
+
};
|
|
129
|
+
}[];
|
|
130
|
+
};
|
|
131
|
+
'ai/reasoning': {
|
|
132
|
+
patch: {
|
|
133
|
+
op: string;
|
|
134
|
+
path: string;
|
|
135
|
+
value: {
|
|
136
|
+
$add: string[];
|
|
137
|
+
};
|
|
138
|
+
}[];
|
|
139
|
+
};
|
|
140
|
+
'ai/activity': {
|
|
141
|
+
patch: {
|
|
142
|
+
op: string;
|
|
143
|
+
path: string;
|
|
144
|
+
value: string;
|
|
145
|
+
}[];
|
|
146
|
+
};
|
|
147
|
+
'ai/reply': {
|
|
148
|
+
patch: ({
|
|
149
|
+
op: string;
|
|
150
|
+
path: string;
|
|
151
|
+
value: {
|
|
152
|
+
role: string;
|
|
153
|
+
content: string;
|
|
154
|
+
};
|
|
155
|
+
} | {
|
|
156
|
+
op: string;
|
|
157
|
+
path: string;
|
|
158
|
+
value: string;
|
|
159
|
+
})[];
|
|
160
|
+
effects: {
|
|
161
|
+
run: string;
|
|
162
|
+
}[];
|
|
163
|
+
};
|
|
164
|
+
'ai/failed': {
|
|
165
|
+
patch: {
|
|
166
|
+
op: string;
|
|
167
|
+
path: string;
|
|
168
|
+
value: string;
|
|
169
|
+
}[];
|
|
170
|
+
};
|
|
171
|
+
'ai/clear': {
|
|
172
|
+
patch: ({
|
|
173
|
+
op: string;
|
|
174
|
+
path: string;
|
|
175
|
+
value: any[];
|
|
176
|
+
} | {
|
|
177
|
+
op: string;
|
|
178
|
+
path: string;
|
|
179
|
+
value: string;
|
|
180
|
+
})[];
|
|
181
|
+
effects: {
|
|
182
|
+
run: string;
|
|
183
|
+
}[];
|
|
184
|
+
};
|
|
185
|
+
'ai/ledger': {
|
|
186
|
+
patch: {
|
|
187
|
+
op: string;
|
|
188
|
+
path: string;
|
|
189
|
+
value: string;
|
|
190
|
+
}[];
|
|
191
|
+
};
|
|
192
|
+
'ai/goal-draft': {
|
|
193
|
+
patch: {
|
|
194
|
+
op: string;
|
|
195
|
+
path: string;
|
|
196
|
+
value: string;
|
|
197
|
+
}[];
|
|
198
|
+
};
|
|
199
|
+
'ai/goal-set': {
|
|
200
|
+
effects: {
|
|
201
|
+
run: string;
|
|
202
|
+
}[];
|
|
203
|
+
};
|
|
204
|
+
'ai/goal-committed': {
|
|
205
|
+
patch: {
|
|
206
|
+
op: string;
|
|
207
|
+
path: string;
|
|
208
|
+
value: string;
|
|
209
|
+
}[];
|
|
210
|
+
};
|
|
211
|
+
'ai/goal-clear': {
|
|
212
|
+
effects: {
|
|
213
|
+
run: string;
|
|
214
|
+
}[];
|
|
215
|
+
};
|
|
216
|
+
'ai/remember': {
|
|
217
|
+
patch: {
|
|
218
|
+
op: string;
|
|
219
|
+
path: string;
|
|
220
|
+
value: boolean;
|
|
221
|
+
}[];
|
|
222
|
+
effects: {
|
|
223
|
+
run: string;
|
|
224
|
+
}[];
|
|
225
|
+
};
|
|
226
|
+
'ai/remembered': {
|
|
227
|
+
patch: ({
|
|
228
|
+
op: string;
|
|
229
|
+
path: string;
|
|
230
|
+
value: boolean;
|
|
231
|
+
} | {
|
|
232
|
+
op: string;
|
|
233
|
+
path: string;
|
|
234
|
+
value: string;
|
|
235
|
+
})[];
|
|
236
|
+
};
|
|
237
|
+
};
|
package/src/actions.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/** Assistant transitions received from the existing panel. */
|
|
3
|
+
export const ASSISTANT_ACTIONS = {
|
|
4
|
+
'ai/cancel': {
|
|
5
|
+
patch: [
|
|
6
|
+
{ op: 'replace', path: '/ai/status', value: 'idle' },
|
|
7
|
+
{ op: 'replace', path: '/ai/pending', value: '' },
|
|
8
|
+
{ op: 'replace', path: '/ai/activity', value: null },
|
|
9
|
+
],
|
|
10
|
+
effects: [{ run: 'ai-cancel' }],
|
|
11
|
+
},
|
|
12
|
+
'ai/toggle': {
|
|
13
|
+
patch: [{ op: 'replace', path: '/ai/open', value: { $not: '$.ai.open' } }],
|
|
14
|
+
// opening while unconfigured pins the settings form open (see
|
|
15
|
+
// ai-ensure-settings), so it cannot vanish mid-edit as typing
|
|
16
|
+
// makes the configuration valid. The ledger read is what makes a
|
|
17
|
+
// reloaded page show the objective it was left with — the goal
|
|
18
|
+
// lives in storage, not in this tab.
|
|
19
|
+
effects: [{ run: 'ai-ensure-settings' }, { run: 'ai-ledger-read' }],
|
|
20
|
+
},
|
|
21
|
+
'ai/settings-toggle': {
|
|
22
|
+
patch: [{ op: 'replace', path: '/ai/settingsOpen', value: { $not: '$.ai.settingsOpen' } }],
|
|
23
|
+
},
|
|
24
|
+
'ai/settings-open': {
|
|
25
|
+
patch: [{ op: 'replace', path: '/ai/settingsOpen', value: '$payload' }],
|
|
26
|
+
},
|
|
27
|
+
'ai/setting': {
|
|
28
|
+
patch: [
|
|
29
|
+
{
|
|
30
|
+
op: 'add',
|
|
31
|
+
path: { $concat: ['/ai/settings/', '$payload.key'] },
|
|
32
|
+
value: '$event.value',
|
|
33
|
+
},
|
|
34
|
+
// an edited connection voids the last probe verdict
|
|
35
|
+
{ op: 'replace', path: '/ai/probe', value: { status: 'idle', detail: null, models: [] } },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
'ai/probe': {
|
|
39
|
+
patch: [{
|
|
40
|
+
op: 'replace',
|
|
41
|
+
path: '/ai/probe',
|
|
42
|
+
value: { status: 'busy', detail: null, models: [] },
|
|
43
|
+
}],
|
|
44
|
+
effects: [{ run: 'ai-probe' }],
|
|
45
|
+
},
|
|
46
|
+
'ai/probe-result': {
|
|
47
|
+
patch: [{ op: 'replace', path: '/ai/probe', value: '$payload' }],
|
|
48
|
+
},
|
|
49
|
+
'ai/save-settings': {
|
|
50
|
+
patch: [{ op: 'replace', path: '/ai/settingsOpen', value: false }],
|
|
51
|
+
effects: [{ run: 'ai-save-settings' }],
|
|
52
|
+
},
|
|
53
|
+
'ai/draft': {
|
|
54
|
+
patch: [{ op: 'replace', path: '/ai/draft', value: '$event.value' }],
|
|
55
|
+
},
|
|
56
|
+
'ai/send': { effects: [{ run: 'ai-send' }] },
|
|
57
|
+
'ai/user': {
|
|
58
|
+
patch: [
|
|
59
|
+
{ op: 'add', path: '/ai/messages/-', value: { role: 'user', content: '$payload' } },
|
|
60
|
+
{ op: 'replace', path: '/ai/draft', value: '' },
|
|
61
|
+
{ op: 'replace', path: '/ai/pending', value: '' },
|
|
62
|
+
{ op: 'replace', path: '/ai/status', value: 'streaming' },
|
|
63
|
+
{ op: 'replace', path: '/ai/activity', value: null },
|
|
64
|
+
{ op: 'replace', path: '/ai/error', value: null },
|
|
65
|
+
{ op: 'replace', path: '/ai/reasoningChars', value: 0 },
|
|
66
|
+
],
|
|
67
|
+
effects: [{ run: 'ai-persist' }],
|
|
68
|
+
},
|
|
69
|
+
'ai/delta': {
|
|
70
|
+
patch: [{ op: 'replace', path: '/ai/pending', value: { $concat: ['$.ai.pending', '$payload'] } }],
|
|
71
|
+
},
|
|
72
|
+
'ai/reasoning': {
|
|
73
|
+
patch: [{ op: 'replace', path: '/ai/reasoningChars', value: { $add: ['$.ai.reasoningChars', '$payload'] } }],
|
|
74
|
+
},
|
|
75
|
+
'ai/activity': {
|
|
76
|
+
patch: [{ op: 'replace', path: '/ai/activity', value: '$payload' }],
|
|
77
|
+
},
|
|
78
|
+
'ai/reply': {
|
|
79
|
+
patch: [
|
|
80
|
+
{ op: 'add', path: '/ai/messages/-', value: { role: 'assistant', content: '$payload' } },
|
|
81
|
+
{ op: 'replace', path: '/ai/pending', value: '' },
|
|
82
|
+
{ op: 'replace', path: '/ai/status', value: 'idle' },
|
|
83
|
+
{ op: 'replace', path: '/ai/activity', value: null },
|
|
84
|
+
],
|
|
85
|
+
effects: [{ run: 'ai-persist' }],
|
|
86
|
+
},
|
|
87
|
+
'ai/failed': {
|
|
88
|
+
patch: [
|
|
89
|
+
{ op: 'replace', path: '/ai/status', value: 'error' },
|
|
90
|
+
{ op: 'replace', path: '/ai/error', value: '$payload' },
|
|
91
|
+
{ op: 'replace', path: '/ai/pending', value: '' },
|
|
92
|
+
{ op: 'replace', path: '/ai/activity', value: null },
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
'ai/clear': {
|
|
96
|
+
patch: [
|
|
97
|
+
{ op: 'replace', path: '/ai/messages', value: [] },
|
|
98
|
+
{ op: 'replace', path: '/ai/pending', value: '' },
|
|
99
|
+
{ op: 'replace', path: '/ai/status', value: 'idle' },
|
|
100
|
+
{ op: 'replace', path: '/ai/activity', value: null },
|
|
101
|
+
{ op: 'replace', path: '/ai/error', value: null },
|
|
102
|
+
{ op: 'replace', path: '/ai/remembered', value: null },
|
|
103
|
+
],
|
|
104
|
+
// the archived rounds go with the transcript they were cut from:
|
|
105
|
+
// their addresses only ever existed in its synopsis
|
|
106
|
+
effects: [{ run: 'ai-clear-run' }, { run: 'ai-persist' }, { run: 'ai-clear-archive' }],
|
|
107
|
+
},
|
|
108
|
+
'ai/ledger': {
|
|
109
|
+
patch: [
|
|
110
|
+
{ op: 'replace', path: '/ai/goal', value: '$payload.goal' },
|
|
111
|
+
{ op: 'replace', path: '/ai/memories', value: '$payload.memories' },
|
|
112
|
+
{ op: 'replace', path: '/ai/archived', value: '$payload.archived' },
|
|
113
|
+
{ op: 'replace', path: '/ai/persistence', value: '$payload.persistence' },
|
|
114
|
+
{ op: 'replace', path: '/ai/retention', value: '$payload.retention' },
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
'ai/goal-draft': {
|
|
118
|
+
patch: [{ op: 'replace', path: '/ai/goalDraft', value: '$event.value' }],
|
|
119
|
+
},
|
|
120
|
+
'ai/goal-set': { effects: [{ run: 'ai-goal-set' }] },
|
|
121
|
+
'ai/goal-committed': {
|
|
122
|
+
patch: [
|
|
123
|
+
{ op: 'replace', path: '/ai/goal', value: '$payload.goal' },
|
|
124
|
+
{ op: 'replace', path: '/ai/memories', value: '$payload.memories' },
|
|
125
|
+
{ op: 'replace', path: '/ai/archived', value: '$payload.archived' },
|
|
126
|
+
{ op: 'replace', path: '/ai/persistence', value: '$payload.persistence' },
|
|
127
|
+
{ op: 'replace', path: '/ai/retention', value: '$payload.retention' },
|
|
128
|
+
{ op: 'replace', path: '/ai/goalDraft', value: '' },
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
'ai/goal-clear': { effects: [{ run: 'ai-goal-clear' }] },
|
|
132
|
+
'ai/remember': {
|
|
133
|
+
patch: [
|
|
134
|
+
{ op: 'replace', path: '/ai/remembering', value: true },
|
|
135
|
+
{ op: 'replace', path: '/ai/remembered', value: null },
|
|
136
|
+
],
|
|
137
|
+
effects: [{ run: 'ai-remember' }],
|
|
138
|
+
},
|
|
139
|
+
'ai/remembered': {
|
|
140
|
+
patch: [
|
|
141
|
+
{ op: 'replace', path: '/ai/remembering', value: false },
|
|
142
|
+
{ op: 'replace', path: '/ai/remembered', value: '$payload.note' },
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** @param {HTMLElement | null} node @param {AssistantOptions} [options] */
|
|
2
|
+
export function mountAssistant(node: HTMLElement | null, options?: AssistantOptions): {
|
|
3
|
+
controller: {
|
|
4
|
+
effects: {
|
|
5
|
+
[k: string]: (props: any, dispatch: any) => void | Promise<any>;
|
|
6
|
+
};
|
|
7
|
+
cancel: () => void;
|
|
8
|
+
dispose: () => any;
|
|
9
|
+
readonly signal: AbortSignal;
|
|
10
|
+
};
|
|
11
|
+
read: () => any;
|
|
12
|
+
dispatch(action: any, payload?: any, event?: any): void;
|
|
13
|
+
subscribe(listener: any): () => void;
|
|
14
|
+
/** Presentation updates retain the transcript, request and input node. */
|
|
15
|
+
update(next: AssistantPresentation): void;
|
|
16
|
+
dispose(): any;
|
|
17
|
+
};
|
|
18
|
+
/** @param {AssistantOptions} [options] */
|
|
19
|
+
export function createAssistantWidget(options?: AssistantOptions): {
|
|
20
|
+
mount: (host: any, props: any) => {
|
|
21
|
+
controller: {
|
|
22
|
+
effects: {
|
|
23
|
+
[k: string]: (props: any, dispatch: any) => void | Promise<any>;
|
|
24
|
+
};
|
|
25
|
+
cancel: () => void;
|
|
26
|
+
dispose: () => any;
|
|
27
|
+
readonly signal: AbortSignal;
|
|
28
|
+
};
|
|
29
|
+
read: () => any;
|
|
30
|
+
dispatch(action: any, payload?: any, event?: any): void;
|
|
31
|
+
subscribe(listener: any): () => void;
|
|
32
|
+
/** Presentation updates retain the transcript, request and input node. */
|
|
33
|
+
update(next: AssistantPresentation): void;
|
|
34
|
+
dispose(): any;
|
|
35
|
+
};
|
|
36
|
+
update: (handle: any, props: any) => any;
|
|
37
|
+
unmount: (handle: any) => any;
|
|
38
|
+
};
|
|
39
|
+
export type AssistantPresentation = {
|
|
40
|
+
title?: string;
|
|
41
|
+
launchLabel?: string;
|
|
42
|
+
launchTitle?: string;
|
|
43
|
+
intro?: string;
|
|
44
|
+
capabilities?: string[];
|
|
45
|
+
settingsHint?: string;
|
|
46
|
+
};
|
|
47
|
+
export type AssistantOptions = AssistantPresentation & Partial<Omit<Parameters<typeof createAssistantController>[0], "getApp">> & {
|
|
48
|
+
settings?: any;
|
|
49
|
+
transcript?: any;
|
|
50
|
+
open?: boolean;
|
|
51
|
+
schedule?: (flush: () => void) => void;
|
|
52
|
+
onError?: (error: Error) => void;
|
|
53
|
+
renderMarkdown?: (source: string) => any;
|
|
54
|
+
};
|
|
55
|
+
import { createAssistantController } from './index.js';
|
package/src/component.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/** The visual assistant owns one app, its request lifecycle and its DOM listeners. */
|
|
3
|
+
import { createApp, formEventFields } from '@jarenjs/app';
|
|
4
|
+
import { createMdComponent } from '@jarenjs/md/component';
|
|
5
|
+
import { highlightPlugin } from '@jarenjs/md/plugins';
|
|
6
|
+
import { mermaidPlugin } from '@jarenjs/mermaid/plugin';
|
|
7
|
+
import { createLedger } from '@tangleai/context';
|
|
8
|
+
import { createToolbox } from '@tangleai/agents';
|
|
9
|
+
import { createAssistantState, createAssistantController, ASSISTANT_ACTIONS } from './index.js';
|
|
10
|
+
import { createAssistantRules } from './views.js';
|
|
11
|
+
import { assistantView } from './viewmodel.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {{ title?: string, launchLabel?: string, launchTitle?: string,
|
|
15
|
+
* intro?: string, capabilities?: string[], settingsHint?: string }} AssistantPresentation
|
|
16
|
+
* @typedef {AssistantPresentation & Partial<Omit<Parameters<typeof createAssistantController>[0], 'getApp'>> & {
|
|
17
|
+
* settings?: any, transcript?: any, open?: boolean,
|
|
18
|
+
* schedule?: (flush: () => void) => void, onError?: (error: Error) => void,
|
|
19
|
+
* renderMarkdown?: (source: string) => any }} AssistantOptions
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_PRESENTATION = {
|
|
22
|
+
title: 'Assistant', launchLabel: ' Assistant', launchTitle: 'Open assistant',
|
|
23
|
+
intro: 'Describe what you want to get done. The assistant can use the tools this host provides.',
|
|
24
|
+
capabilities: [],
|
|
25
|
+
settingsHint: 'Provider requests use your settings. This host controls local persistence.',
|
|
26
|
+
};
|
|
27
|
+
let nextInstance = 0;
|
|
28
|
+
/** @param {AssistantPresentation} value */
|
|
29
|
+
function presentation(value) {
|
|
30
|
+
return Object.fromEntries(Object.entries(value).filter(([key]) => Object.hasOwn(DEFAULT_PRESENTATION, key)));
|
|
31
|
+
}
|
|
32
|
+
/** @param {HTMLElement | null} node @param {AssistantOptions} [options] */
|
|
33
|
+
export function mountAssistant(node, options = {}) {
|
|
34
|
+
let app, disposed = false, closing;
|
|
35
|
+
const id = `tangle-assistant-${++nextInstance}`;
|
|
36
|
+
let settings = options.settings ?? null, transcript = options.transcript ?? null;
|
|
37
|
+
const aiStorage = options.aiStorage ?? { read: () => settings, write: value => { settings = structuredClone(value); } };
|
|
38
|
+
const aiChat = options.aiChat ?? { read: () => transcript, write: value => { transcript = structuredClone(value); } };
|
|
39
|
+
const md = createMdComponent({ plugins: [highlightPlugin(), mermaidPlugin({ theme: 'host' })], headingIds: true, headingAnchors: true });
|
|
40
|
+
const renderMarkdown = options.renderMarkdown ?? (source => md.view(source, { slugPrefix: `user-content-${id}-` }));
|
|
41
|
+
const controller = createAssistantController({ ...options, aiStorage, aiChat,
|
|
42
|
+
ledger: options.ledger ?? createLedger(), toolbox: options.toolbox ?? createToolbox(), getApp: () => app });
|
|
43
|
+
app = createApp({
|
|
44
|
+
state: { ai: { ...createAssistantState(aiStorage.read(), aiChat.read()), open: options.open ?? false }, presentation: { ...DEFAULT_PRESENTATION, ...presentation(options) } },
|
|
45
|
+
actions: { ...ASSISTANT_ACTIONS, 'assistant/presentation': { patch: [{ op: 'replace', path: '/presentation', value: '$payload' }] } },
|
|
46
|
+
view: { $jslt: '0.1', modes: { assistant: { unmatched: 'error' } }, rules: [
|
|
47
|
+
{ match: '$', body: ['section', { class: 'tangle-assistant', 'data-assistant-instance': id }, { $apply: ['$.ui.assistant', 'assistant'] }] },
|
|
48
|
+
...createAssistantRules(`${id}-models`),
|
|
49
|
+
] },
|
|
50
|
+
}, {
|
|
51
|
+
node, document: node?.ownerDocument, schedule: options.schedule, onError: options.onError,
|
|
52
|
+
eventFields: formEventFields(), effects: controller.effects,
|
|
53
|
+
afterRender: () => { if (node?.querySelectorAll) md.hydrate(node); },
|
|
54
|
+
viewModel: state => ({ ui: { assistant: { ...assistantView(state.ai, renderMarkdown), ...state.presentation,
|
|
55
|
+
capabilities: state.presentation.capabilities.map(text => ({ text })) } } }),
|
|
56
|
+
});
|
|
57
|
+
// A Markdown fragment belongs to this component, not to its host's hash router.
|
|
58
|
+
function click(event) {
|
|
59
|
+
const anchor = event.target.closest?.('a[href^="#"]');
|
|
60
|
+
const href = anchor?.getAttribute('href');
|
|
61
|
+
if (!href || href.startsWith('#/')) return;
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
let fragment; try { fragment = decodeURIComponent(href.slice(1)); } catch { return; }
|
|
64
|
+
for (const target of node?.querySelectorAll('[id]') ?? []) {
|
|
65
|
+
if (target.id === fragment) { target.scrollIntoView?.({ block: 'nearest' }); break; }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
node?.addEventListener('click', click);
|
|
69
|
+
if (options.open) {
|
|
70
|
+
controller.effects['ai-ensure-settings']({}, app.dispatch);
|
|
71
|
+
controller.effects['ai-ledger-read']({}, app.dispatch);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
controller,
|
|
75
|
+
read: () => structuredClone(app.getState().ai),
|
|
76
|
+
dispatch(action, payload = null, event = null) { if (!disposed) app.dispatch(action, payload, event); },
|
|
77
|
+
subscribe(listener) { return app.subscribe(state => listener(structuredClone(state.ai))); },
|
|
78
|
+
/** Presentation updates retain the transcript, request and input node. */
|
|
79
|
+
update(/** @type {AssistantPresentation} */ next) {
|
|
80
|
+
if (!disposed) app.dispatch('assistant/presentation', { ...app.getState().presentation, ...presentation(next) });
|
|
81
|
+
},
|
|
82
|
+
dispose() {
|
|
83
|
+
if (disposed) return closing;
|
|
84
|
+
disposed = true; node?.removeEventListener('click', click);
|
|
85
|
+
closing = controller.dispose(); app.destroy(); return closing;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** @param {AssistantOptions} [options] */
|
|
90
|
+
export function createAssistantWidget(options = {}) {
|
|
91
|
+
return { mount: (host, props) => mountAssistant(host, { ...options, ...props }),
|
|
92
|
+
update: (handle, props) => handle.update(props), unmount: handle => handle.dispose() };
|
|
93
|
+
}
|