ikanban-web 0.2.7 → 0.2.8

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.
@@ -1,5 +1,10 @@
1
- import { Binary } from "ikanban-utils/binary"
2
- import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
1
+ import { Binary } from "ikanban-utils/binary";
2
+ import {
3
+ produce,
4
+ reconcile,
5
+ type SetStoreFunction,
6
+ type Store,
7
+ } from "solid-js/store";
3
8
  import type {
4
9
  FileDiff,
5
10
  Message,
@@ -10,33 +15,36 @@ import type {
10
15
  Session,
11
16
  SessionStatus,
12
17
  Todo,
13
- } from "@opencode-ai/sdk/v2/client"
14
- import type { State, VcsCache } from "./types"
15
- import { trimSessions } from "./session-trim"
18
+ } from "@opencode-ai/sdk/v2/client";
19
+ import type { State, VcsCache } from "./types";
20
+ import { trimSessions } from "./session-trim";
16
21
 
17
22
  export function applyGlobalEvent(input: {
18
- event: { type: string; properties?: unknown }
19
- project: Project[]
20
- setGlobalProject: (next: Project[] | ((draft: Project[]) => void)) => void
21
- refresh: () => void
23
+ event: { type: string; properties?: unknown };
24
+ project: Project[];
25
+ setGlobalProject: (next: Project[] | ((draft: Project[]) => void)) => void;
26
+ refresh: () => void;
22
27
  }) {
23
- if (input.event.type === "global.disposed" || input.event.type === "server.connected") {
24
- input.refresh()
25
- return
28
+ if (
29
+ input.event.type === "global.disposed" ||
30
+ input.event.type === "server.connected"
31
+ ) {
32
+ input.refresh();
33
+ return;
26
34
  }
27
35
 
28
- if (input.event.type !== "project.updated") return
29
- const properties = input.event.properties as Project
30
- const result = Binary.search(input.project, properties.id, (s) => s.id)
36
+ if (input.event.type !== "project.updated") return;
37
+ const properties = input.event.properties as Project;
38
+ const result = Binary.search(input.project, properties.id, (s) => s.id);
31
39
  if (result.found) {
32
40
  input.setGlobalProject((draft) => {
33
- draft[result.index] = { ...draft[result.index], ...properties }
34
- })
35
- return
41
+ draft[result.index] = { ...draft[result.index], ...properties };
42
+ });
43
+ return;
36
44
  }
37
45
  input.setGlobalProject((draft) => {
38
- draft.splice(result.index, 0, properties)
39
- })
46
+ draft.splice(result.index, 0, properties);
47
+ });
40
48
  }
41
49
 
42
50
  function cleanupSessionCaches(
@@ -45,302 +53,374 @@ function cleanupSessionCaches(
45
53
  sessionID: string,
46
54
  setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void,
47
55
  ) {
48
- if (!sessionID) return
56
+ if (!sessionID) return;
49
57
  const hasAny =
50
58
  store.message[sessionID] !== undefined ||
51
59
  store.session_diff[sessionID] !== undefined ||
52
60
  store.todo[sessionID] !== undefined ||
53
61
  store.permission[sessionID] !== undefined ||
54
62
  store.question[sessionID] !== undefined ||
55
- store.session_status[sessionID] !== undefined
56
- setSessionTodo?.(sessionID, undefined)
57
- if (!hasAny) return
63
+ store.session_status[sessionID] !== undefined;
64
+ setSessionTodo?.(sessionID, undefined);
65
+ if (!hasAny) return;
58
66
  setStore(
59
67
  produce((draft) => {
60
- const messages = draft.message[sessionID]
68
+ const messages = draft.message[sessionID];
61
69
  if (messages) {
62
70
  for (const message of messages) {
63
- const id = message?.id
64
- if (!id) continue
65
- delete draft.part[id]
71
+ const id = message?.id;
72
+ if (!id) continue;
73
+ delete draft.part[id];
66
74
  }
67
75
  }
68
- delete draft.message[sessionID]
69
- delete draft.session_diff[sessionID]
70
- delete draft.todo[sessionID]
71
- delete draft.permission[sessionID]
72
- delete draft.question[sessionID]
73
- delete draft.session_status[sessionID]
76
+ delete draft.message[sessionID];
77
+ delete draft.session_diff[sessionID];
78
+ delete draft.todo[sessionID];
79
+ delete draft.permission[sessionID];
80
+ delete draft.question[sessionID];
81
+ delete draft.session_status[sessionID];
74
82
  }),
75
- )
83
+ );
76
84
  }
77
85
 
78
86
  export function applyDirectoryEvent(input: {
79
- event: { type: string; properties?: unknown }
80
- store: Store<State>
81
- setStore: SetStoreFunction<State>
82
- push: (directory: string) => void
83
- directory: string
84
- loadLsp: () => void
85
- vcsCache?: VcsCache
86
- setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void
87
+ event: { type: string; properties?: unknown };
88
+ store: Store<State>;
89
+ setStore: SetStoreFunction<State>;
90
+ push: (directory: string) => void;
91
+ directory: string;
92
+ loadLsp: () => void;
93
+ vcsCache?: VcsCache;
94
+ setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void;
87
95
  }) {
88
- const event = input.event
96
+ const event = input.event;
89
97
  switch (event.type) {
90
98
  case "server.instance.disposed": {
91
- input.push(input.directory)
92
- return
99
+ input.push(input.directory);
100
+ return;
93
101
  }
94
102
  case "session.created": {
95
- const info = (event.properties as { info: Session }).info
96
- const result = Binary.search(input.store.session, info.id, (s) => s.id)
103
+ const info = (event.properties as { info: Session }).info;
104
+ const result = Binary.search(input.store.session, info.id, (s) => s.id);
97
105
  if (result.found) {
98
- input.setStore("session", result.index, reconcile(info))
99
- break
106
+ input.setStore("session", result.index, reconcile(info));
107
+ break;
100
108
  }
101
- const next = input.store.session.slice()
102
- next.splice(result.index, 0, info)
103
- const trimmed = trimSessions(next, { limit: input.store.limit, permission: input.store.permission })
104
- input.setStore("session", reconcile(trimmed, { key: "id" }))
105
- if (!info.parentID) input.setStore("sessionTotal", (value) => value + 1)
106
- break
109
+ const next = input.store.session.slice();
110
+ next.splice(result.index, 0, info);
111
+ const trimmed = trimSessions(next, {
112
+ limit: input.store.limit,
113
+ permission: input.store.permission,
114
+ });
115
+ input.setStore("session", reconcile(trimmed, { key: "id" }));
116
+ if (!info.parentID) input.setStore("sessionTotal", (value) => value + 1);
117
+ break;
107
118
  }
108
119
  case "session.updated": {
109
- const info = (event.properties as { info: Session }).info
110
- const result = Binary.search(input.store.session, info.id, (s) => s.id)
120
+ const info = (event.properties as { info: Session }).info;
121
+ const result = Binary.search(input.store.session, info.id, (s) => s.id);
111
122
  if (info.time.archived) {
112
123
  if (result.found) {
113
124
  input.setStore(
114
125
  "session",
115
126
  produce((draft) => {
116
- draft.splice(result.index, 1)
127
+ draft.splice(result.index, 1);
117
128
  }),
118
- )
129
+ );
119
130
  }
120
- cleanupSessionCaches(input.store, input.setStore, info.id, input.setSessionTodo)
121
- if (info.parentID) break
122
- input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
123
- break
131
+ cleanupSessionCaches(
132
+ input.store,
133
+ input.setStore,
134
+ info.id,
135
+ input.setSessionTodo,
136
+ );
137
+ if (info.parentID) break;
138
+ input.setStore("sessionTotal", (value) => Math.max(0, value - 1));
139
+ break;
124
140
  }
125
141
  if (result.found) {
126
- input.setStore("session", result.index, reconcile(info))
127
- break
142
+ input.setStore("session", result.index, reconcile(info));
143
+ break;
128
144
  }
129
- const next = input.store.session.slice()
130
- next.splice(result.index, 0, info)
131
- const trimmed = trimSessions(next, { limit: input.store.limit, permission: input.store.permission })
132
- input.setStore("session", reconcile(trimmed, { key: "id" }))
133
- break
145
+ const next = input.store.session.slice();
146
+ next.splice(result.index, 0, info);
147
+ const trimmed = trimSessions(next, {
148
+ limit: input.store.limit,
149
+ permission: input.store.permission,
150
+ });
151
+ input.setStore("session", reconcile(trimmed, { key: "id" }));
152
+ break;
134
153
  }
135
154
  case "session.deleted": {
136
- const info = (event.properties as { info: Session }).info
137
- const result = Binary.search(input.store.session, info.id, (s) => s.id)
155
+ const info = (event.properties as { info: Session }).info;
156
+ const result = Binary.search(input.store.session, info.id, (s) => s.id);
138
157
  if (result.found) {
139
158
  input.setStore(
140
159
  "session",
141
160
  produce((draft) => {
142
- draft.splice(result.index, 1)
161
+ draft.splice(result.index, 1);
143
162
  }),
144
- )
163
+ );
145
164
  }
146
- cleanupSessionCaches(input.store, input.setStore, info.id, input.setSessionTodo)
147
- if (info.parentID) break
148
- input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
149
- break
165
+ cleanupSessionCaches(
166
+ input.store,
167
+ input.setStore,
168
+ info.id,
169
+ input.setSessionTodo,
170
+ );
171
+ if (info.parentID) break;
172
+ input.setStore("sessionTotal", (value) => Math.max(0, value - 1));
173
+ break;
150
174
  }
151
175
  case "session.diff": {
152
- const props = event.properties as { sessionID: string; diff: FileDiff[] }
153
- input.setStore("session_diff", props.sessionID, reconcile(props.diff, { key: "file" }))
154
- break
176
+ const props = event.properties as { sessionID: string; diff: FileDiff[] };
177
+ input.setStore(
178
+ "session_diff",
179
+ props.sessionID,
180
+ reconcile(props.diff, { key: "file" }),
181
+ );
182
+ break;
155
183
  }
156
184
  case "todo.updated": {
157
- const props = event.properties as { sessionID: string; todos: Todo[] }
158
- input.setStore("todo", props.sessionID, reconcile(props.todos, { key: "id" }))
159
- input.setSessionTodo?.(props.sessionID, props.todos)
160
- break
185
+ const props = event.properties as { sessionID: string; todos: Todo[] };
186
+ input.setStore(
187
+ "todo",
188
+ props.sessionID,
189
+ reconcile(props.todos, { key: "id" }),
190
+ );
191
+ input.setSessionTodo?.(props.sessionID, props.todos);
192
+ break;
161
193
  }
162
194
  case "session.status": {
163
- const props = event.properties as { sessionID: string; status: SessionStatus }
164
- input.setStore("session_status", props.sessionID, reconcile(props.status))
165
- break
195
+ const props = event.properties as {
196
+ sessionID: string;
197
+ status: SessionStatus;
198
+ };
199
+ if (props.status.type === "idle") {
200
+ input.setStore(
201
+ produce((draft) => {
202
+ delete draft.session_diff[props.sessionID];
203
+ delete draft.project_diff[input.directory];
204
+ }),
205
+ );
206
+ }
207
+ input.setStore(
208
+ "session_status",
209
+ props.sessionID,
210
+ reconcile(props.status),
211
+ );
212
+ break;
166
213
  }
167
214
  case "message.updated": {
168
- const info = (event.properties as { info: Message }).info
169
- const messages = input.store.message[info.sessionID]
215
+ const info = (event.properties as { info: Message }).info;
216
+ const messages = input.store.message[info.sessionID];
170
217
  if (!messages) {
171
- input.setStore("message", info.sessionID, [info])
172
- break
218
+ input.setStore("message", info.sessionID, [info]);
219
+ break;
173
220
  }
174
- const result = Binary.search(messages, info.id, (m) => m.id)
221
+ const result = Binary.search(messages, info.id, (m) => m.id);
175
222
  if (result.found) {
176
- input.setStore("message", info.sessionID, result.index, reconcile(info))
177
- break
223
+ input.setStore(
224
+ "message",
225
+ info.sessionID,
226
+ result.index,
227
+ reconcile(info),
228
+ );
229
+ break;
178
230
  }
179
231
  input.setStore(
180
232
  "message",
181
233
  info.sessionID,
182
234
  produce((draft) => {
183
- draft.splice(result.index, 0, info)
235
+ draft.splice(result.index, 0, info);
184
236
  }),
185
- )
186
- break
237
+ );
238
+ break;
187
239
  }
188
240
  case "message.removed": {
189
- const props = event.properties as { sessionID: string; messageID: string }
241
+ const props = event.properties as {
242
+ sessionID: string;
243
+ messageID: string;
244
+ };
190
245
  input.setStore(
191
246
  produce((draft) => {
192
- const messages = draft.message[props.sessionID]
247
+ const messages = draft.message[props.sessionID];
193
248
  if (messages) {
194
- const result = Binary.search(messages, props.messageID, (m) => m.id)
195
- if (result.found) messages.splice(result.index, 1)
249
+ const result = Binary.search(
250
+ messages,
251
+ props.messageID,
252
+ (m) => m.id,
253
+ );
254
+ if (result.found) messages.splice(result.index, 1);
196
255
  }
197
- delete draft.part[props.messageID]
256
+ delete draft.part[props.messageID];
198
257
  }),
199
- )
200
- break
258
+ );
259
+ break;
201
260
  }
202
261
  case "message.part.updated": {
203
- const part = (event.properties as { part: Part }).part
204
- const parts = input.store.part[part.messageID]
262
+ const part = (event.properties as { part: Part }).part;
263
+ const parts = input.store.part[part.messageID];
205
264
  if (!parts) {
206
- input.setStore("part", part.messageID, [part])
207
- break
265
+ input.setStore("part", part.messageID, [part]);
266
+ break;
208
267
  }
209
- const result = Binary.search(parts, part.id, (p) => p.id)
268
+ const result = Binary.search(parts, part.id, (p) => p.id);
210
269
  if (result.found) {
211
- input.setStore("part", part.messageID, result.index, reconcile(part))
212
- break
270
+ input.setStore("part", part.messageID, result.index, reconcile(part));
271
+ break;
213
272
  }
214
273
  input.setStore(
215
274
  "part",
216
275
  part.messageID,
217
276
  produce((draft) => {
218
- draft.splice(result.index, 0, part)
277
+ draft.splice(result.index, 0, part);
219
278
  }),
220
- )
221
- break
279
+ );
280
+ break;
222
281
  }
223
282
  case "message.part.removed": {
224
- const props = event.properties as { messageID: string; partID: string }
225
- const parts = input.store.part[props.messageID]
226
- if (!parts) break
227
- const result = Binary.search(parts, props.partID, (p) => p.id)
283
+ const props = event.properties as { messageID: string; partID: string };
284
+ const parts = input.store.part[props.messageID];
285
+ if (!parts) break;
286
+ const result = Binary.search(parts, props.partID, (p) => p.id);
228
287
  if (result.found) {
229
288
  input.setStore(
230
289
  produce((draft) => {
231
- const list = draft.part[props.messageID]
232
- if (!list) return
233
- const next = Binary.search(list, props.partID, (p) => p.id)
234
- if (!next.found) return
235
- list.splice(next.index, 1)
236
- if (list.length === 0) delete draft.part[props.messageID]
290
+ const list = draft.part[props.messageID];
291
+ if (!list) return;
292
+ const next = Binary.search(list, props.partID, (p) => p.id);
293
+ if (!next.found) return;
294
+ list.splice(next.index, 1);
295
+ if (list.length === 0) delete draft.part[props.messageID];
237
296
  }),
238
- )
297
+ );
239
298
  }
240
- break
299
+ break;
241
300
  }
242
301
  case "message.part.delta": {
243
- const props = event.properties as { messageID: string; partID: string; field: string; delta: string }
244
- const parts = input.store.part[props.messageID]
245
- if (!parts) break
246
- const result = Binary.search(parts, props.partID, (p) => p.id)
247
- if (!result.found) break
302
+ const props = event.properties as {
303
+ messageID: string;
304
+ partID: string;
305
+ field: string;
306
+ delta: string;
307
+ };
308
+ const parts = input.store.part[props.messageID];
309
+ if (!parts) break;
310
+ const result = Binary.search(parts, props.partID, (p) => p.id);
311
+ if (!result.found) break;
248
312
  input.setStore(
249
313
  "part",
250
314
  props.messageID,
251
315
  produce((draft) => {
252
- const part = draft[result.index]
253
- const field = props.field as keyof typeof part
254
- const existing = part[field] as string | undefined
255
- ;(part[field] as string) = (existing ?? "") + props.delta
316
+ const part = draft[result.index];
317
+ const field = props.field as keyof typeof part;
318
+ const existing = part[field] as string | undefined;
319
+ (part[field] as string) = (existing ?? "") + props.delta;
256
320
  }),
257
- )
258
- break
321
+ );
322
+ break;
259
323
  }
260
324
  case "vcs.branch.updated": {
261
- const props = event.properties as { branch: string }
262
- if (input.store.vcs?.branch === props.branch) break
263
- const next = { branch: props.branch }
264
- input.setStore("vcs", next)
265
- if (input.vcsCache) input.vcsCache.setStore("value", next)
266
- break
325
+ const props = event.properties as { branch: string };
326
+ if (input.store.vcs?.branch === props.branch) break;
327
+ const next = { branch: props.branch };
328
+ input.setStore("vcs", next);
329
+ if (input.vcsCache) input.vcsCache.setStore("value", next);
330
+ break;
267
331
  }
268
332
  case "permission.asked": {
269
- const permission = event.properties as PermissionRequest
270
- const permissions = input.store.permission[permission.sessionID]
333
+ const permission = event.properties as PermissionRequest;
334
+ const permissions = input.store.permission[permission.sessionID];
271
335
  if (!permissions) {
272
- input.setStore("permission", permission.sessionID, [permission])
273
- break
336
+ input.setStore("permission", permission.sessionID, [permission]);
337
+ break;
274
338
  }
275
- const result = Binary.search(permissions, permission.id, (p) => p.id)
339
+ const result = Binary.search(permissions, permission.id, (p) => p.id);
276
340
  if (result.found) {
277
- input.setStore("permission", permission.sessionID, result.index, reconcile(permission))
278
- break
341
+ input.setStore(
342
+ "permission",
343
+ permission.sessionID,
344
+ result.index,
345
+ reconcile(permission),
346
+ );
347
+ break;
279
348
  }
280
349
  input.setStore(
281
350
  "permission",
282
351
  permission.sessionID,
283
352
  produce((draft) => {
284
- draft.splice(result.index, 0, permission)
353
+ draft.splice(result.index, 0, permission);
285
354
  }),
286
- )
287
- break
355
+ );
356
+ break;
288
357
  }
289
358
  case "permission.replied": {
290
- const props = event.properties as { sessionID: string; requestID: string }
291
- const permissions = input.store.permission[props.sessionID]
292
- if (!permissions) break
293
- const result = Binary.search(permissions, props.requestID, (p) => p.id)
294
- if (!result.found) break
359
+ const props = event.properties as {
360
+ sessionID: string;
361
+ requestID: string;
362
+ };
363
+ const permissions = input.store.permission[props.sessionID];
364
+ if (!permissions) break;
365
+ const result = Binary.search(permissions, props.requestID, (p) => p.id);
366
+ if (!result.found) break;
295
367
  input.setStore(
296
368
  "permission",
297
369
  props.sessionID,
298
370
  produce((draft) => {
299
- draft.splice(result.index, 1)
371
+ draft.splice(result.index, 1);
300
372
  }),
301
- )
302
- break
373
+ );
374
+ break;
303
375
  }
304
376
  case "question.asked": {
305
- const question = event.properties as QuestionRequest
306
- const questions = input.store.question[question.sessionID]
377
+ const question = event.properties as QuestionRequest;
378
+ const questions = input.store.question[question.sessionID];
307
379
  if (!questions) {
308
- input.setStore("question", question.sessionID, [question])
309
- break
380
+ input.setStore("question", question.sessionID, [question]);
381
+ break;
310
382
  }
311
- const result = Binary.search(questions, question.id, (q) => q.id)
383
+ const result = Binary.search(questions, question.id, (q) => q.id);
312
384
  if (result.found) {
313
- input.setStore("question", question.sessionID, result.index, reconcile(question))
314
- break
385
+ input.setStore(
386
+ "question",
387
+ question.sessionID,
388
+ result.index,
389
+ reconcile(question),
390
+ );
391
+ break;
315
392
  }
316
393
  input.setStore(
317
394
  "question",
318
395
  question.sessionID,
319
396
  produce((draft) => {
320
- draft.splice(result.index, 0, question)
397
+ draft.splice(result.index, 0, question);
321
398
  }),
322
- )
323
- break
399
+ );
400
+ break;
324
401
  }
325
402
  case "question.replied":
326
403
  case "question.rejected": {
327
- const props = event.properties as { sessionID: string; requestID: string }
328
- const questions = input.store.question[props.sessionID]
329
- if (!questions) break
330
- const result = Binary.search(questions, props.requestID, (q) => q.id)
331
- if (!result.found) break
404
+ const props = event.properties as {
405
+ sessionID: string;
406
+ requestID: string;
407
+ };
408
+ const questions = input.store.question[props.sessionID];
409
+ if (!questions) break;
410
+ const result = Binary.search(questions, props.requestID, (q) => q.id);
411
+ if (!result.found) break;
332
412
  input.setStore(
333
413
  "question",
334
414
  props.sessionID,
335
415
  produce((draft) => {
336
- draft.splice(result.index, 1)
416
+ draft.splice(result.index, 1);
337
417
  }),
338
- )
339
- break
418
+ );
419
+ break;
340
420
  }
341
421
  case "lsp.updated": {
342
- input.loadLsp()
343
- break
422
+ input.loadLsp();
423
+ break;
344
424
  }
345
425
  }
346
426
  }
@@ -49,6 +49,9 @@ export type State = {
49
49
  session_diff: {
50
50
  [sessionID: string]: FileDiff[]
51
51
  }
52
+ project_diff: {
53
+ [directory: string]: FileDiff[]
54
+ }
52
55
  todo: {
53
56
  [sessionID: string]: Todo[]
54
57
  }