flexdesk 0.17.18 → 0.18.0

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.
Files changed (118) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +1 -0
  3. package/css/flexdesk.css +7544 -5916
  4. package/css/reset.css +1 -1
  5. package/css/tokens.css +23 -1
  6. package/dist/charts.js +10 -6
  7. package/dist/charts.js.map +1 -1
  8. package/dist/chunk-5W4EEKSK.js +778 -0
  9. package/dist/chunk-5W4EEKSK.js.map +7 -0
  10. package/dist/chunk-DNJ3N4T4.js +82 -0
  11. package/dist/chunk-DNJ3N4T4.js.map +7 -0
  12. package/dist/{chunk-REJ5HLIM.js → chunk-HYJYYEPK.js} +2 -2
  13. package/dist/{chunk-7U4IB65H.js → chunk-LLBJ2SJH.js} +5 -3
  14. package/dist/{chunk-7U4IB65H.js.map → chunk-LLBJ2SJH.js.map} +1 -1
  15. package/dist/{chunk-7OL6F2AM.js → chunk-P74F6I6P.js} +6 -4
  16. package/dist/{chunk-7OL6F2AM.js.map → chunk-P74F6I6P.js.map} +1 -1
  17. package/dist/chunk-PAI2ZUWV.js +260 -0
  18. package/dist/chunk-PAI2ZUWV.js.map +7 -0
  19. package/dist/chunk-Q7BALEPX.js +527 -0
  20. package/dist/chunk-Q7BALEPX.js.map +7 -0
  21. package/dist/chunk-QXAOTGCC.js +40 -0
  22. package/dist/chunk-QXAOTGCC.js.map +7 -0
  23. package/dist/{chunk-XJNADRGR.js → chunk-RKVYIXDC.js} +664 -91
  24. package/dist/chunk-RKVYIXDC.js.map +7 -0
  25. package/dist/{chunk-2Z2ENHR3.js → chunk-U5GKJUOC.js} +3 -3
  26. package/dist/{chunk-2Z2ENHR3.js.map → chunk-U5GKJUOC.js.map} +1 -1
  27. package/dist/chunk-UKP22RUG.js +219 -0
  28. package/dist/chunk-UKP22RUG.js.map +7 -0
  29. package/dist/chunk-VLOAI3HS.js +43 -0
  30. package/dist/chunk-VLOAI3HS.js.map +7 -0
  31. package/dist/{chunk-WUFLGETN.js → chunk-WVFGV5FT.js} +2 -39
  32. package/dist/{chunk-WUFLGETN.js.map → chunk-WVFGV5FT.js.map} +3 -3
  33. package/dist/{chunk-PGZ4EHSE.js → chunk-ZL3VKINP.js} +41 -390
  34. package/dist/chunk-ZL3VKINP.js.map +7 -0
  35. package/dist/editor.js +3 -2
  36. package/dist/editor.js.map +1 -1
  37. package/dist/flexdesk.css +7544 -5916
  38. package/dist/flow.js +29 -97
  39. package/dist/flow.js.map +3 -3
  40. package/dist/git.js +1356 -0
  41. package/dist/git.js.map +7 -0
  42. package/dist/reset.css +1 -1
  43. package/dist/tiles.js +20 -15
  44. package/dist/tiles.js.map +2 -2
  45. package/dist/tokens.css +23 -1
  46. package/dist/widgets.js +3967 -1188
  47. package/dist/widgets.js.map +4 -4
  48. package/dist/wm.js +120 -32
  49. package/dist/wm.js.map +2 -2
  50. package/docs/README.md +9 -4
  51. package/docs/best-practices.md +2 -2
  52. package/docs/components/chat-shell.md +198 -0
  53. package/docs/components/conversation.md +10 -0
  54. package/docs/components/filter-builder.md +246 -0
  55. package/docs/components/forms-and-dialogs.md +22 -0
  56. package/docs/components/git.md +355 -0
  57. package/docs/components/input-fields.md +167 -2
  58. package/docs/components/keymap.md +8 -0
  59. package/docs/components/menus.md +82 -2
  60. package/docs/components/panels-and-desktops.md +16 -5
  61. package/docs/components/shell.md +104 -1
  62. package/docs/components/workspace-picker.md +149 -0
  63. package/docs/concepts/building-and-packaging.md +2 -2
  64. package/docs/concepts/packages-and-entry-points.md +14 -13
  65. package/docs/concepts/state-guard.md +3 -3
  66. package/docs/quickstart.md +1 -1
  67. package/git.js +13 -0
  68. package/package.json +6 -1
  69. package/src/flow/kit/dom.js +28 -119
  70. package/src/flow/kit/step_picker.js +1 -1
  71. package/src/flow/kit/value_picker.js +1 -1
  72. package/src/flow/outline/menu.js +1 -1
  73. package/src/git/diff_viewer.js +212 -0
  74. package/src/git/diff_window.js +51 -0
  75. package/src/git/git_graph.js +401 -0
  76. package/src/git/git_ops.js +61 -0
  77. package/src/git/git_panel.js +755 -0
  78. package/src/git/index.js +9 -0
  79. package/src/git/line_diff.js +202 -0
  80. package/src/tiles/tile_base.js +5 -4
  81. package/src/tiling/desktops.js +10 -8
  82. package/src/tiling/keymap.js +6 -5
  83. package/src/tiling/kind_taxonomy.js +15 -2
  84. package/src/tiling/shell.js +124 -17
  85. package/src/tiling/tile_renderer.js +2 -3
  86. package/src/ui/components/action_dropdown.js +4 -78
  87. package/src/ui/components/anchored_layer.js +255 -0
  88. package/src/ui/components/chat_shell.js +920 -0
  89. package/src/ui/components/chat_state_machine.js +223 -0
  90. package/src/ui/components/choice_controls.js +45 -2
  91. package/src/ui/components/color_field.js +3 -2
  92. package/src/ui/components/color_list_field.js +3 -2
  93. package/src/ui/components/combobox.js +422 -0
  94. package/src/ui/components/conversation_view.js +17 -2
  95. package/src/ui/components/data_table.js +2 -2
  96. package/src/ui/components/filter_builder.js +766 -0
  97. package/src/ui/components/filter_model.js +614 -0
  98. package/src/ui/components/managed_window.js +24 -47
  99. package/src/ui/components/modal.js +79 -109
  100. package/src/ui/components/option_list.js +160 -0
  101. package/src/ui/components/popover.js +94 -0
  102. package/src/ui/components/recent_items.js +169 -0
  103. package/src/ui/components/select.js +322 -0
  104. package/src/ui/components/tag_input.js +77 -8
  105. package/src/ui/components/workspace_picker.js +1155 -0
  106. package/src/ui/utils/fire_event.js +17 -0
  107. package/src/ui/utils/focusable.js +25 -0
  108. package/src/ui/utils/modal_host.js +27 -0
  109. package/widgets.js +12 -2
  110. package/css/base.css +0 -10547
  111. package/css/overrides.css +0 -2596
  112. package/dist/chunk-7JI7HHSY.js +0 -107
  113. package/dist/chunk-7JI7HHSY.js.map +0 -7
  114. package/dist/chunk-MIW7D6WC.js +0 -526
  115. package/dist/chunk-MIW7D6WC.js.map +0 -7
  116. package/dist/chunk-PGZ4EHSE.js.map +0 -7
  117. package/dist/chunk-XJNADRGR.js.map +0 -7
  118. /package/dist/{chunk-REJ5HLIM.js.map → chunk-HYJYYEPK.js.map} +0 -0
@@ -0,0 +1,223 @@
1
+ /**
2
+ * ══ THE CONVERSATION STATE MACHINE ════════════════════════════════════════
3
+ *
4
+ * The lifecycle of one assistant conversation, with no view and no transport in
5
+ * it. `mountChatShell` (`./chat_shell.js`) drives one; a chat of your own can
6
+ * too.
7
+ *
8
+ * idle ──send──▶ waiting ──streamStart──▶ streaming ──streamComplete──▶ idle
9
+ * └─ streamComplete ▶ idle ├─ cancel ▶ idle
10
+ * └─ cancel ▶ idle └─ error ▶ error
11
+ * └─ error ▶ error
12
+ * error ──send──▶ waiting error ──reset──▶ idle
13
+ *
14
+ * TRANSITIONS ARE QUEUED. `StateMachine` (`../../core/state_machine.js`) is
15
+ * asynchronous and refuses a transition that arrives during another. Text can
16
+ * arrive in the same tick as the request that asked for it, so every call here
17
+ * waits for the one before it, in call order, and answers a promise of
18
+ * `{ transitioned, reason? }`. `getState()` therefore lags a call by some
19
+ * microtasks; `settled()` resolves when the queue is empty, and `getState()` is
20
+ * `null` until the machine has entered `idle`. A transition the current state
21
+ * does not have is refused, not thrown.
22
+ *
23
+ * OBSERVERS: `onChange({ state, conversationId, mode })` hears every state
24
+ * entered. With an `eventBus`, the same goes out as `CHAT_BUS_EVENTS.STATE_CHANGED`
25
+ * along with the others below.
26
+ *
27
+ * THE MODE is an opaque string of the caller's (`null` for none): it is kept
28
+ * and handed back, and means nothing here.
29
+ */
30
+
31
+ import { StateMachine } from '../../core/state_machine.js';
32
+
33
+ export const CHAT_STATES = Object.freeze({
34
+ IDLE: 'idle',
35
+ WAITING: 'waiting',
36
+ STREAMING: 'streaming',
37
+ ERROR: 'error',
38
+ });
39
+
40
+ export const CHAT_TRANSITIONS = Object.freeze({
41
+ SEND: 'chat:message:send',
42
+ STREAM_START: 'chat:stream:start',
43
+ STREAM_COMPLETE: 'chat:stream:complete',
44
+ CANCEL: 'chat:cancel',
45
+ ERROR: 'chat:error',
46
+ RESET: 'chat:reset',
47
+ });
48
+
49
+ export const CHAT_BUS_EVENTS = Object.freeze({
50
+ STATE_CHANGED: 'chat:state:changed',
51
+ MESSAGE_SENT: 'chat:message:sent',
52
+ STREAM_COMPLETE: 'chat:stream:completed',
53
+ MODE_CHANGED: 'chat:mode:changed',
54
+ CONVERSATION_NEW: 'chat:conversation:new',
55
+ CONVERSATION_LOADED: 'chat:conversation:loaded',
56
+ ERROR: 'chat:error:raised',
57
+ });
58
+
59
+ export class ChatStateMachine {
60
+ #machine;
61
+ #eventBus;
62
+ #onChange;
63
+ #queue = Promise.resolve();
64
+ #conversationId = null;
65
+ #mode;
66
+ #lastError = null;
67
+
68
+ /**
69
+ * @param {object} [deps]
70
+ * @param {object} [deps.eventBus] gets `CHAT_BUS_EVENTS`
71
+ * @param {object} [deps.logger]
72
+ * @param {?string} [deps.mode] the mode to start in
73
+ * @param {Function} [deps.onChange] `({ state, conversationId, mode })` on every state entered
74
+ */
75
+ constructor({ eventBus = null, logger = null, mode = null, onChange = null } = {}) {
76
+ this.#eventBus = eventBus;
77
+ this.#onChange = onChange;
78
+ this.#mode = mode;
79
+
80
+ const entered = () => this.#entered();
81
+ const send = {
82
+ target: CHAT_STATES.WAITING,
83
+ guard: (ctx) => !!ctx.payload?.message,
84
+ action: (ctx) => this.#messageSent(ctx.payload),
85
+ };
86
+ const inFlight = {
87
+ [CHAT_TRANSITIONS.STREAM_COMPLETE]: {
88
+ target: CHAT_STATES.IDLE,
89
+ action: (ctx) => this.#emit(CHAT_BUS_EVENTS.STREAM_COMPLETE, {
90
+ conversationId: this.#conversationId, ...ctx.payload,
91
+ }),
92
+ },
93
+ [CHAT_TRANSITIONS.CANCEL]: { target: CHAT_STATES.IDLE },
94
+ [CHAT_TRANSITIONS.ERROR]: {
95
+ target: CHAT_STATES.ERROR,
96
+ action: (ctx) => this.#failed(ctx.payload),
97
+ },
98
+ [CHAT_TRANSITIONS.RESET]: { target: CHAT_STATES.IDLE },
99
+ };
100
+
101
+ this.#machine = new StateMachine({
102
+ name: 'chat',
103
+ initialState: CHAT_STATES.IDLE,
104
+ eventBus,
105
+ logger,
106
+ states: {
107
+ [CHAT_STATES.IDLE]: {
108
+ onEnter: () => {
109
+ this.#lastError = null;
110
+ entered();
111
+ },
112
+ transitions: { [CHAT_TRANSITIONS.SEND]: send },
113
+ },
114
+ [CHAT_STATES.WAITING]: {
115
+ onEnter: entered,
116
+ transitions: {
117
+ [CHAT_TRANSITIONS.STREAM_START]: { target: CHAT_STATES.STREAMING },
118
+ ...inFlight,
119
+ },
120
+ },
121
+ [CHAT_STATES.STREAMING]: { onEnter: entered, transitions: inFlight },
122
+ [CHAT_STATES.ERROR]: {
123
+ onEnter: entered,
124
+ transitions: {
125
+ [CHAT_TRANSITIONS.RESET]: { target: CHAT_STATES.IDLE },
126
+ [CHAT_TRANSITIONS.SEND]: send,
127
+ },
128
+ },
129
+ },
130
+ });
131
+ }
132
+
133
+ getState() { return this.#machine.getState(); }
134
+
135
+ get conversationId() { return this.#conversationId; }
136
+ set conversationId(id) { this.#conversationId = id ?? null; }
137
+
138
+ get mode() { return this.#mode; }
139
+ get lastError() { return this.#lastError; }
140
+
141
+ setMode(mode) {
142
+ this.#mode = mode;
143
+ this.#emit(CHAT_BUS_EVENTS.MODE_CHANGED, { mode });
144
+ }
145
+
146
+ /** Resolves when every call so far has taken effect. */
147
+ settled() { return this.#queue; }
148
+
149
+ sendMessage(message, conversationId = this.#conversationId) {
150
+ this.#conversationId = conversationId ?? null;
151
+ return this.#transition(CHAT_TRANSITIONS.SEND, { message });
152
+ }
153
+
154
+ streamStart() { return this.#transition(CHAT_TRANSITIONS.STREAM_START); }
155
+
156
+ /** @param {object} [payload] goes out with `CHAT_BUS_EVENTS.STREAM_COMPLETE` */
157
+ streamComplete(payload = {}) { return this.#transition(CHAT_TRANSITIONS.STREAM_COMPLETE, payload); }
158
+
159
+ cancel() { return this.#transition(CHAT_TRANSITIONS.CANCEL); }
160
+
161
+ error(message, detail = null) { return this.#transition(CHAT_TRANSITIONS.ERROR, { message, detail }); }
162
+
163
+ reset() { return this.#transition(CHAT_TRANSITIONS.RESET); }
164
+
165
+ /** Back to idle with no conversation, whatever the state. */
166
+ newConversation() {
167
+ this.#conversationId = null;
168
+ return this.#enqueue(async () => {
169
+ await this.#machine.reset(CHAT_STATES.IDLE);
170
+ this.#emit(CHAT_BUS_EVENTS.CONVERSATION_NEW, {});
171
+ });
172
+ }
173
+
174
+ /** Back to idle on a kept conversation, whatever the state. */
175
+ loadConversation(conversationId) {
176
+ this.#conversationId = conversationId ?? null;
177
+ return this.#enqueue(async () => {
178
+ await this.#machine.reset(CHAT_STATES.IDLE);
179
+ this.#emit(CHAT_BUS_EVENTS.CONVERSATION_LOADED, { conversationId });
180
+ });
181
+ }
182
+
183
+ canSend() {
184
+ const state = this.getState();
185
+ return state === CHAT_STATES.IDLE || state === CHAT_STATES.ERROR;
186
+ }
187
+
188
+ isProcessing() {
189
+ const state = this.getState();
190
+ return state === CHAT_STATES.WAITING || state === CHAT_STATES.STREAMING;
191
+ }
192
+
193
+ #transition(event, payload) {
194
+ return this.#enqueue(() => this.#machine.transition(event, payload));
195
+ }
196
+
197
+ /** One step after the last; a step that fails rejects its own promise and not the next one's. */
198
+ #enqueue(step) {
199
+ const run = this.#queue.then(step);
200
+ this.#queue = run.then(() => undefined, () => undefined);
201
+ return run;
202
+ }
203
+
204
+ #messageSent({ message }) {
205
+ this.#lastError = null;
206
+ this.#emit(CHAT_BUS_EVENTS.MESSAGE_SENT, {
207
+ conversationId: this.#conversationId, text: message, mode: this.#mode,
208
+ });
209
+ }
210
+
211
+ #failed({ message, detail }) {
212
+ this.#lastError = message;
213
+ this.#emit(CHAT_BUS_EVENTS.ERROR, { conversationId: this.#conversationId, message, detail });
214
+ }
215
+
216
+ #entered() {
217
+ const change = { state: this.getState(), conversationId: this.#conversationId, mode: this.#mode };
218
+ this.#onChange?.(change);
219
+ this.#emit(CHAT_BUS_EVENTS.STATE_CHANGED, change);
220
+ }
221
+
222
+ #emit(name, payload) { this.#eventBus?.emit(name, payload); }
223
+ }
@@ -47,10 +47,15 @@ function el(tag, className, text) {
47
47
 
48
48
  const isGroup = (o) => o !== null && typeof o === 'object' && Array.isArray(o.options) && 'group' in o;
49
49
 
50
- /** One option, normalised: `{value, label, description, disabled, reason}`. */
50
+ /**
51
+ * One option, normalised: `{value, label, description, disabled, reason, icon, hint,
52
+ * keywords, source}`. `icon`, `hint` and `keywords` are the DOM-drawn select's and
53
+ * combobox's; `source` is the entry as it was given, handed back when one is chosen.
54
+ */
51
55
  function choiceOption(o) {
52
56
  if (o === null || typeof o !== 'object') {
53
- return { value: o, label: shownValue(o), description: null, disabled: false, reason: null };
57
+ return { value: o, label: shownValue(o), description: null, disabled: false, reason: null, icon: null,
58
+ hint: null, keywords: null, source: o };
54
59
  }
55
60
  return {
56
61
  value: o.value,
@@ -58,6 +63,10 @@ function choiceOption(o) {
58
63
  description: o.description != null ? String(o.description) : null,
59
64
  disabled: Boolean(o.disabled),
60
65
  reason: o.reason != null && o.reason !== '' ? String(o.reason) : null,
66
+ icon: o.icon ? String(o.icon) : null,
67
+ hint: o.hint != null && o.hint !== '' ? String(o.hint) : null,
68
+ keywords: o.keywords != null ? String(o.keywords) : null,
69
+ source: o,
61
70
  };
62
71
  }
63
72
 
@@ -71,6 +80,40 @@ export function flatChoices(options) {
71
80
  return out;
72
81
  }
73
82
 
83
+ /**
84
+ * A list as the sections it draws in, in order: `[{ heading, items }]`, `heading`
85
+ * null for a run of options outside any group, `items` normalised. A group with no
86
+ * options is left out.
87
+ */
88
+ export function choiceSections(options) {
89
+ const out = [];
90
+ for (const o of options || []) {
91
+ if (isGroup(o)) {
92
+ if (o.options.length) out.push({ heading: String(o.group ?? ''), items: o.options.map(choiceOption) });
93
+ } else if (out.length && out[out.length - 1].heading === null) {
94
+ out[out.length - 1].items.push(choiceOption(o));
95
+ } else {
96
+ out.push({ heading: null, items: [choiceOption(o)] });
97
+ }
98
+ }
99
+ return out;
100
+ }
101
+
102
+ /** Does `text` occur, ignoring case, in the option's label, value, description or keywords? */
103
+ export function optionMatches(option, text) {
104
+ const needle = String(text ?? '').trim().toLowerCase();
105
+ if (!needle) return true;
106
+ return [option.label, shownValue(option.value), option.description, option.keywords]
107
+ .some((part) => String(part ?? '').toLowerCase().includes(needle));
108
+ }
109
+
110
+ /** `sections` with only the options `text` matches; a section left empty goes. */
111
+ export function filterSections(sections, text) {
112
+ return sections
113
+ .map((s) => ({ heading: s.heading, items: s.items.filter((o) => optionMatches(o, text)) }))
114
+ .filter((s) => s.items.length);
115
+ }
116
+
74
117
  /**
75
118
  * Where the chips and the radio list draw a group heading: the position in
76
119
  * the flattened list of each non-empty group's first option, to its heading.
@@ -28,6 +28,7 @@
28
28
  * the box's `change` (blur, Enter, a preset, the picker closing) with
29
29
  * `commit: true`.
30
30
  */
31
+ import { fireEvent } from '../utils/fire_event.js';
31
32
 
32
33
  const HEX_RE = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
33
34
  const RGB_RE = /^rgba?\(\s*([^)]*)\)$/i;
@@ -197,8 +198,8 @@ export class ColorField {
197
198
  /** Put `text` in the box and fire the box's own events, as typing would. */
198
199
  #writeBox(text, commit) {
199
200
  this.hex.value = text;
200
- this.hex.dispatchEvent(new Event('input', { bubbles: true }));
201
- if (commit) this.hex.dispatchEvent(new Event('change', { bubbles: true }));
201
+ fireEvent(this.hex, 'input', { bubbles: true });
202
+ if (commit) fireEvent(this.hex, 'change', { bubbles: true });
202
203
  }
203
204
 
204
205
  /** Swatch and invalid mark follow the box; `commit === null` is silent. */
@@ -27,6 +27,7 @@
27
27
 
28
28
  import { SortableList } from './sortable_list.js';
29
29
  import { ColorField, isHexColor, normalizeHex } from './color_field.js';
30
+ import { fireEvent } from '../utils/fire_event.js';
30
31
 
31
32
  let _seq = 0;
32
33
 
@@ -202,8 +203,8 @@ export class ColorListField {
202
203
  #emit(commit) {
203
204
  this.#writeInput();
204
205
  if (this.input) {
205
- this.input.dispatchEvent(new Event('input', { bubbles: true }));
206
- if (commit) this.input.dispatchEvent(new Event('change', { bubbles: true }));
206
+ fireEvent(this.input, 'input', { bubbles: true });
207
+ if (commit) fireEvent(this.input, 'change', { bubbles: true });
207
208
  }
208
209
  this.onChange?.(this.getValue(), { commit });
209
210
  }