pith-ui 0.1.0 → 0.1.4

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 (124) hide show
  1. package/AI.md +20 -1
  2. package/API.md +834 -272
  3. package/README.md +3 -1
  4. package/dist/composer.d.ts.map +1 -1
  5. package/dist/composer.js +48 -29
  6. package/dist/composer.js.map +1 -1
  7. package/dist/content/conversation-reader.d.ts.map +1 -1
  8. package/dist/content/conversation-reader.js +8 -1
  9. package/dist/content/conversation-reader.js.map +1 -1
  10. package/dist/content/conversation-view.d.ts +21 -4
  11. package/dist/content/conversation-view.d.ts.map +1 -1
  12. package/dist/content/conversation-view.js +72 -13
  13. package/dist/content/conversation-view.js.map +1 -1
  14. package/dist/content/image-viewer.d.ts +4 -2
  15. package/dist/content/image-viewer.d.ts.map +1 -1
  16. package/dist/content/image-viewer.js +36 -15
  17. package/dist/content/image-viewer.js.map +1 -1
  18. package/dist/content/paged-preview.d.ts +1 -1
  19. package/dist/content/paged-preview.d.ts.map +1 -1
  20. package/dist/content/paged-preview.js +10 -5
  21. package/dist/content/paged-preview.js.map +1 -1
  22. package/dist/content/views.d.ts +3 -0
  23. package/dist/content/views.d.ts.map +1 -1
  24. package/dist/content/views.js +110 -41
  25. package/dist/content/views.js.map +1 -1
  26. package/dist/content/welcome.d.ts.map +1 -1
  27. package/dist/content/welcome.js +19 -7
  28. package/dist/content/welcome.js.map +1 -1
  29. package/dist/dom.js +1 -1
  30. package/dist/dom.js.map +1 -1
  31. package/dist/files/panels.d.ts +8 -1
  32. package/dist/files/panels.d.ts.map +1 -1
  33. package/dist/files/panels.js +235 -120
  34. package/dist/files/panels.js.map +1 -1
  35. package/dist/files/systems.d.ts.map +1 -1
  36. package/dist/files/systems.js +81 -91
  37. package/dist/files/systems.js.map +1 -1
  38. package/dist/forms/actions.d.ts +28 -7
  39. package/dist/forms/actions.d.ts.map +1 -1
  40. package/dist/forms/actions.js +59 -11
  41. package/dist/forms/actions.js.map +1 -1
  42. package/dist/forms/custom-connection.d.ts +2 -1
  43. package/dist/forms/custom-connection.d.ts.map +1 -1
  44. package/dist/forms/custom-connection.js +36 -28
  45. package/dist/forms/custom-connection.js.map +1 -1
  46. package/dist/forms/dialog-frame.d.ts +2 -0
  47. package/dist/forms/dialog-frame.d.ts.map +1 -1
  48. package/dist/forms/dialog-frame.js +46 -26
  49. package/dist/forms/dialog-frame.js.map +1 -1
  50. package/dist/forms/history.d.ts +1 -1
  51. package/dist/forms/history.d.ts.map +1 -1
  52. package/dist/forms/history.js +78 -15
  53. package/dist/forms/history.js.map +1 -1
  54. package/dist/forms/identity.d.ts +12 -0
  55. package/dist/forms/identity.d.ts.map +1 -1
  56. package/dist/forms/identity.js +16 -0
  57. package/dist/forms/identity.js.map +1 -1
  58. package/dist/forms/model-editor.d.ts +8 -3
  59. package/dist/forms/model-editor.d.ts.map +1 -1
  60. package/dist/forms/model-editor.js +73 -62
  61. package/dist/forms/model-editor.js.map +1 -1
  62. package/dist/forms/panels.d.ts +103 -7
  63. package/dist/forms/panels.d.ts.map +1 -1
  64. package/dist/forms/panels.js +183 -77
  65. package/dist/forms/panels.js.map +1 -1
  66. package/dist/forms/provider-login.d.ts.map +1 -1
  67. package/dist/forms/provider-login.js +25 -39
  68. package/dist/forms/provider-login.js.map +1 -1
  69. package/dist/forms/resource-editor.d.ts +9 -5
  70. package/dist/forms/resource-editor.d.ts.map +1 -1
  71. package/dist/forms/resource-editor.js +20 -9
  72. package/dist/forms/resource-editor.js.map +1 -1
  73. package/dist/foundations.d.ts +95 -0
  74. package/dist/foundations.d.ts.map +1 -0
  75. package/dist/foundations.js +299 -0
  76. package/dist/foundations.js.map +1 -0
  77. package/dist/navigation/context-menu.d.ts +8 -1
  78. package/dist/navigation/context-menu.d.ts.map +1 -1
  79. package/dist/navigation/context-menu.js +67 -50
  80. package/dist/navigation/context-menu.js.map +1 -1
  81. package/dist/navigation/navigation.d.ts +55 -7
  82. package/dist/navigation/navigation.d.ts.map +1 -1
  83. package/dist/navigation/navigation.js +167 -18
  84. package/dist/navigation/navigation.js.map +1 -1
  85. package/dist/operation.d.ts +12 -0
  86. package/dist/operation.d.ts.map +1 -0
  87. package/dist/operation.js +49 -0
  88. package/dist/operation.js.map +1 -0
  89. package/dist/primitives.d.ts +11 -1
  90. package/dist/primitives.d.ts.map +1 -1
  91. package/dist/primitives.js +164 -301
  92. package/dist/primitives.js.map +1 -1
  93. package/dist/reconcile.d.ts +11 -0
  94. package/dist/reconcile.d.ts.map +1 -0
  95. package/dist/reconcile.js +36 -0
  96. package/dist/reconcile.js.map +1 -0
  97. package/dist/select.d.ts +14 -0
  98. package/dist/select.d.ts.map +1 -0
  99. package/dist/select.js +304 -0
  100. package/dist/select.js.map +1 -0
  101. package/dist/styles.css +452 -382
  102. package/dist/surface.d.ts +60 -0
  103. package/dist/surface.d.ts.map +1 -0
  104. package/dist/surface.js +344 -0
  105. package/dist/surface.js.map +1 -0
  106. package/dist/tasks/approval.d.ts.map +1 -1
  107. package/dist/tasks/approval.js +86 -45
  108. package/dist/tasks/approval.js.map +1 -1
  109. package/dist/tasks/permission-element.d.ts +3 -0
  110. package/dist/tasks/permission-element.d.ts.map +1 -0
  111. package/dist/tasks/permission-element.js +10 -0
  112. package/dist/tasks/permission-element.js.map +1 -0
  113. package/dist/tasks/permission.d.ts +5 -3
  114. package/dist/tasks/permission.d.ts.map +1 -1
  115. package/dist/tasks/permission.js +47 -22
  116. package/dist/tasks/permission.js.map +1 -1
  117. package/dist/tasks/runtime.d.ts.map +1 -1
  118. package/dist/tasks/runtime.js +30 -13
  119. package/dist/tasks/runtime.js.map +1 -1
  120. package/dist/workspace.d.ts +58 -4
  121. package/dist/workspace.d.ts.map +1 -1
  122. package/dist/workspace.js +202 -44
  123. package/dist/workspace.js.map +1 -1
  124. package/package.json +1 -1
@@ -1,4 +1,6 @@
1
- import type { UIBlock } from '../core.js';
1
+ import { type ActionResult, type UIBlock, type UIAction } from '../core.js';
2
+ import { type ModelEditorState, type ModelSelection } from './model-editor.js';
3
+ export { modelEditorMarkup } from './model-editor.js';
2
4
  export interface FormFeedback {
3
5
  error?: string;
4
6
  disabled?: boolean;
@@ -10,11 +12,63 @@ export interface FormDOMBlock<State extends FormFeedback> extends UIBlock<State>
10
12
  export interface SettingsPanelState extends FormFeedback {
11
13
  baseUrl: string;
12
14
  hasApiKey: boolean;
15
+ ready?: boolean;
16
+ testing?: boolean;
17
+ appearance?: {
18
+ mode: 'light' | 'dark' | 'system';
19
+ disabled?: boolean;
20
+ error?: string;
21
+ };
22
+ testResult?: {
23
+ message: string;
24
+ ok?: boolean;
25
+ };
13
26
  }
14
27
  export interface FormOptions {
15
28
  idPrefix?: string;
16
29
  }
17
- export declare function createSettingsPanel(state: SettingsPanelState, options?: FormOptions): FormDOMBlock<SettingsPanelState>;
30
+ export interface SettingsDraft {
31
+ provider: string;
32
+ baseUrl: string;
33
+ apiKey: string;
34
+ }
35
+ export interface SettingsProviderOption {
36
+ id: string;
37
+ name: string;
38
+ configured?: boolean;
39
+ signedIn?: boolean;
40
+ }
41
+ export interface SettingsCredentialView {
42
+ status: string;
43
+ hidden: boolean;
44
+ required: boolean;
45
+ placeholder: string;
46
+ removeVisible: boolean;
47
+ hint?: string;
48
+ actions?: Node;
49
+ }
50
+ export interface SettingsPanel extends FormDOMBlock<SettingsPanelState> {
51
+ read(): SettingsDraft;
52
+ setProviders(providers: readonly SettingsProviderOption[], selected?: string): void;
53
+ resetProvider(value: {
54
+ provider: string;
55
+ baseUrl: string;
56
+ clearKey?: boolean;
57
+ collapseAdvanced?: boolean;
58
+ }): void;
59
+ setCredential(view: SettingsCredentialView): void;
60
+ clearSecrets(): void;
61
+ readonly updates: HTMLElement;
62
+ }
63
+ export interface SettingsPanelOptions extends FormOptions {
64
+ onAppearanceChange?: (mode: string) => void;
65
+ onProviderChange?: (provider: string) => void;
66
+ onDraftChange?: (draft: SettingsDraft) => void;
67
+ onSubmit?: (draft: SettingsDraft) => ActionResult | Promise<ActionResult>;
68
+ onTest?: (draft: SettingsDraft) => void;
69
+ onRemoveKey?: (draft: SettingsDraft) => ActionResult | Promise<ActionResult>;
70
+ }
71
+ export declare function createSettingsPanel(state: SettingsPanelState, options?: SettingsPanelOptions): SettingsPanel;
18
72
  export interface MCPEditorValue {
19
73
  name: string;
20
74
  type: string;
@@ -28,13 +82,48 @@ export interface MCPEditorValue {
28
82
  hasBearerToken?: boolean;
29
83
  envKeys?: readonly string[];
30
84
  }
31
- export interface MCPConnectionsPanel extends FormDOMBlock<FormFeedback> {
85
+ export interface MCPConnectionDraft {
86
+ name: string;
87
+ type: 'http' | 'stdio';
88
+ url: string;
89
+ oauth: boolean;
90
+ oauthClientId: string;
91
+ oauthScope: string;
92
+ command: string;
93
+ args: string;
94
+ enabled: boolean;
95
+ bearerToken: string;
96
+ clearBearerToken: boolean;
97
+ env: string;
98
+ clearEnv: boolean;
99
+ }
100
+ export interface MCPConnectionsState extends FormFeedback {
101
+ formError?: string;
102
+ }
103
+ export interface MCPConnectionsPanel extends FormDOMBlock<MCPConnectionsState> {
32
104
  renderType(): void;
33
105
  edit(value?: MCPEditorValue): void;
106
+ read(): MCPConnectionDraft;
107
+ clearBearerToken(): void;
108
+ clearSecrets(): void;
109
+ toggleEnvironment(): void;
110
+ readonly list: HTMLElement;
34
111
  }
35
- export declare function createMCPConnectionsPanel(state?: FormFeedback, options?: FormOptions): MCPConnectionsPanel;
36
- export declare function modelEditorMarkup(prefix: string): string;
37
- export declare function createModelPicker(state?: FormFeedback, options?: FormOptions): FormDOMBlock<FormFeedback>;
112
+ export declare function createMCPConnectionsPanel(state?: MCPConnectionsState, options?: FormOptions & {
113
+ onSubmit?: (draft: MCPConnectionDraft) => ActionResult | Promise<ActionResult>;
114
+ }): MCPConnectionsPanel;
115
+ export interface ModelPickerState extends FormFeedback {
116
+ editor?: ModelEditorState;
117
+ loading?: boolean;
118
+ }
119
+ export interface ModelPicker extends FormDOMBlock<ModelPickerState> {
120
+ read(): ModelSelection;
121
+ reset(selection: ModelSelection): void;
122
+ focus(): void;
123
+ }
124
+ export declare function createModelPicker(state?: ModelPickerState, options?: FormOptions & {
125
+ onSubmit?: (selection: ModelSelection) => ActionResult | Promise<ActionResult>;
126
+ }): ModelPicker;
38
127
  export interface MCPConnectionItem {
39
128
  name: string;
40
129
  type: string;
@@ -53,5 +142,12 @@ export interface MCPConnectionItem {
53
142
  export interface MCPConnectionListState extends FormFeedback {
54
143
  entries: readonly MCPConnectionItem[];
55
144
  }
56
- export declare function createMCPConnectionList(initial: MCPConnectionListState): UIBlock<MCPConnectionListState>;
145
+ export type MCPConnectionAction = 'mcp-login' | 'mcp-logout' | 'mcp-edit' | 'mcp-toggle' | 'mcp-remove';
146
+ export interface MCPConnectionTarget {
147
+ readonly name: string;
148
+ }
149
+ export interface MCPConnectionListOptions {
150
+ onAction?: (action: MCPConnectionAction, target: MCPConnectionTarget) => ReturnType<UIAction>;
151
+ }
152
+ export declare function createMCPConnectionList(initial: MCPConnectionListState, options?: MCPConnectionListOptions): UIBlock<MCPConnectionListState>;
57
153
  //# sourceMappingURL=panels.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"panels.d.ts","sourceRoot":"","sources":["../../src/forms/panels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAI1C,MAAM,WAAW,YAAY;IAAG,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE;AACpE,MAAM,WAAW,YAAY,CAAC,KAAK,SAAS,YAAY,CAAE,SAAQ,OAAO,CAAC,KAAK,CAAC;IAC9E,KAAK,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAAE,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;IAC1D,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;CAC7B;AA0BD,MAAM,WAAW,kBAAmB,SAAQ,YAAY;IAAG,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE;AAChG,MAAM,WAAW,WAAW;IAAG,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE;AAClD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,EAAE,OAAO,GAAE,WAAgB,GAAG,YAAY,CAAC,kBAAkB,CAAC,CAuD1H;AACD,MAAM,WAAW,cAAc;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAC;AAC9P,MAAM,WAAW,mBAAoB,SAAQ,YAAY,CAAC,YAAY,CAAC;IAAE,UAAU,IAAI,IAAI,CAAC;IAAC,IAAI,CAAC,KAAK,CAAC,EAAE,cAAc,GAAG,IAAI,CAAA;CAAC;AAChI,wBAAgB,yBAAyB,CAAC,KAAK,GAAE,YAAiB,EAAE,OAAO,GAAE,WAAgB,GAAG,mBAAmB,CAwBlH;AACD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAshC;AAC/kC,wBAAgB,iBAAiB,CAAC,KAAK,GAAE,YAAiB,EAAE,OAAO,GAAE,WAAgB,GAAG,YAAY,CAAC,YAAY,CAAC,CAEjH;AACD,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IACvG,cAAc,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACjJ;AACD,MAAM,WAAW,sBAAuB,SAAQ,YAAY;IAAG,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAA;CAAE;AAEtG,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAmCxG"}
1
+ {"version":3,"file":"panels.d.ts","sourceRoot":"","sources":["../../src/forms/panels.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,KAAK,YAAY,EAAE,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAC,MAAM,YAAY,CAAC;AAG1E,OAAO,EAAuC,KAAK,gBAAgB,EAAE,KAAK,cAAc,EAAC,MAAM,mBAAmB,CAAC;AAInH,OAAO,EAAC,iBAAiB,EAAC,MAAM,mBAAmB,CAAC;AAEpD,MAAM,WAAW,YAAY;IAAG,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE;AACpE,MAAM,WAAW,YAAY,CAAC,KAAK,SAAS,YAAY,CAAE,SAAQ,OAAO,CAAC,KAAK,CAAC;IAC9E,KAAK,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAAE,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;IAC1D,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;CAC7B;AA0BD,MAAM,WAAW,kBAAmB,SAAQ,YAAY;IAAG,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE;QAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAC,CAAC;IAAC,UAAU,CAAC,EAAE;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,OAAO,CAAA;KAAC,CAAA;CAAE;AACxQ,MAAM,WAAW,WAAW;IAAG,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE;AAClD,MAAM,WAAW,aAAa;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC;AAClF,MAAM,WAAW,sBAAsB;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAC;AAC5G,MAAM,WAAW,sBAAsB;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,IAAI,CAAA;CAAC;AACxK,MAAM,WAAW,aAAc,SAAQ,YAAY,CAAC,kBAAkB,CAAC;IAAE,IAAI,IAAI,aAAa,CAAC;IAAC,YAAY,CAAC,SAAS,EAAE,SAAS,sBAAsB,EAAE,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,aAAa,CAAC,KAAK,EAAE;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;KAAC,GAAG,IAAI,CAAC;IAAC,aAAa,CAAC,IAAI,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAAC,YAAY,IAAI,IAAI,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;CAAC;AAC7Y,MAAM,WAAW,oBAAqB,SAAQ,WAAW;IAAE,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAAC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;CAAC;AACxY,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,EAAE,OAAO,GAAE,oBAAyB,GAAG,aAAa,CA8FhH;AACD,MAAM,WAAW,cAAc;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAC;AAC9P,MAAM,WAAW,kBAAkB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAC;AACnR,MAAM,WAAW,mBAAoB,SAAQ,YAAY;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAC;AAC9E,MAAM,WAAW,mBAAoB,SAAQ,YAAY,CAAC,mBAAmB,CAAC;IAAE,UAAU,IAAI,IAAI,CAAC;IAAC,IAAI,CAAC,KAAK,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IAAC,IAAI,IAAI,kBAAkB,CAAC;IAAC,gBAAgB,IAAI,IAAI,CAAC;IAAC,YAAY,IAAI,IAAI,CAAC;IAAC,iBAAiB,IAAI,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAC;AAC1Q,wBAAgB,yBAAyB,CAAC,KAAK,GAAE,mBAAwB,EAAE,OAAO,GAAE,WAAW,GAAG;IAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;CAAM,GAAG,mBAAmB,CAiC5M;AACD,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IAAE,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAC;AACrG,MAAM,WAAW,WAAY,SAAQ,YAAY,CAAC,gBAAgB,CAAC;IAAE,IAAI,IAAI,cAAc,CAAC;IAAC,KAAK,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI,CAAC;IAAC,KAAK,IAAI,IAAI,CAAA;CAAC;AACnJ,wBAAgB,iBAAiB,CAAC,KAAK,GAAE,gBAAqB,EAAE,OAAO,GAAE,WAAW,GAAG;IAAC,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,cAAc,KAAK,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;CAAM,GAAG,WAAW,CAUzL;AACD,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IACvG,cAAc,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACjJ;AACD,MAAM,WAAW,sBAAuB,SAAQ,YAAY;IAAG,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAA;CAAE;AACtG,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,YAAY,GAAG,UAAU,GAAG,YAAY,GAAG,YAAY,CAAC;AACxG,MAAM,WAAW,mBAAmB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAC;AAC5D,MAAM,WAAW,wBAAwB;IAAE,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,EAAE,MAAM,EAAE,mBAAmB,KAAK,UAAU,CAAC,QAAQ,CAAC,CAAA;CAAC;AAEzI,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,sBAAsB,EAAE,OAAO,GAAE,wBAA6B,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAuChJ"}
@@ -1,10 +1,17 @@
1
- import { scopeIds } from './identity.js';
1
+ import {} from '../core.js';
2
+ import { scopeIds, bindFormSubmission } from './identity.js';
2
3
  import { dialogHeading, escape, icon, uniqueId, setText } from '../dom.js';
4
+ import { createModelEditor, modelEditorMarkup } from './model-editor.js';
5
+ import { adoptAction, adoptProductParts, reconcileSelectOptions } from '../foundations.js';
6
+ import { createOperation } from '../operation.js';
7
+ import { reconcileKeyed } from '../reconcile.js';
8
+ export { modelEditorMarkup } from './model-editor.js';
3
9
  function formBlock(markup, className, initial, prefix = '') {
4
10
  const element = document.createElement('div');
5
11
  element.className = className;
6
12
  element.innerHTML = markup;
7
13
  scopeIds(element, prefix);
14
+ const parts = adoptProductParts(element);
8
15
  let destroyed = false;
9
16
  const field = (id) => {
10
17
  const result = Array.from(element.querySelectorAll('[id]')).find((node) => node.getAttribute('id') === prefix + id);
@@ -18,20 +25,20 @@ function formBlock(markup, className, initial, prefix = '') {
18
25
  if (state.error !== undefined) {
19
26
  const error = element.querySelector('[data-dialog-error]') || element.querySelector('.pui-form-error[role="alert"]');
20
27
  if (error)
21
- error.textContent = state.error;
28
+ parts.feedback(error).update({ message: state.error, tone: 'error' });
22
29
  }
23
30
  if (state.disabled !== undefined) {
24
31
  for (const control of element.querySelectorAll('button[type="submit"]')) {
25
- control.disabled = state.disabled || control.dataset.controlPending === 'true';
32
+ parts.action(control).update({ disabled: state.disabled, pending: control.dataset.controlPending === 'true' });
26
33
  }
27
34
  }
28
35
  };
29
36
  update(initial);
30
- return { element, field, id: (id) => prefix + id, update, destroy() { if (destroyed)
31
- return; destroyed = true; element.remove(); } };
37
+ return { element, field, parts, id: (id) => prefix + id, update, destroy() { if (destroyed)
38
+ return; destroyed = true; parts.destroy(); element.remove(); } };
32
39
  }
33
40
  export function createSettingsPanel(state, options = {}) {
34
- return formBlock(`
41
+ const block = formBlock(`
35
42
  ${dialogHeading('Settings', 'settings-title', 'settings-dialog', 'MAKE IT YOURS')}
36
43
  <section class="pui-settings-section pui-appearance-settings" aria-labelledby="appearance-title">
37
44
  <h3 id="appearance-title">Appearance</h3>
@@ -83,63 +90,167 @@ export function createSettingsPanel(state, options = {}) {
83
90
  <p class="pui-field-hint">Version, usage and failure category only; no conversation, file contents, paths or credentials.</p>
84
91
  <div class="pui-settings-actions"><button type="button" class="pui-secondary-button" data-action="diagnostics">Save diagnostics</button></div>
85
92
  </section>
86
- <div class="pui-modal-footer"><button class="pui-secondary-button" type="button" data-close="settings-dialog">Cancel</button><button class="pui-primary-button" id="save-settings" data-control-pending="true" disabled data-global-idle type="submit">Save settings</button></div>
93
+ <div class="pui-modal-footer"><button class="pui-secondary-button" type="button" data-pui-close data-close="settings-dialog">Cancel</button><button class="pui-primary-button" id="save-settings" data-control-pending="true" disabled data-global-idle type="submit">Save settings</button></div>
87
94
  </form>`, 'pui-settings-panel', state, options.idPrefix ?? uniqueId('pui-form') + '-');
95
+ const $ = block.field, abort = new AbortController();
96
+ let current = state, pending = false, removing = false, destroyed = false, operationKind = 'submit';
97
+ // These controls also exist before providers or appearance state have loaded.
98
+ // Adopt them eagerly so the first rendered frame uses the shared selector.
99
+ block.parts.control($('appearance-mode'));
100
+ block.parts.control($('settings-provider'));
101
+ const form = $('settings-form'), submit = $('save-settings'), test = $('test-connection'), remove = $('remove-key');
102
+ const read = () => { const key = $('api-key'); return { provider: block.parts.control($('settings-provider')).getValue(), baseUrl: block.parts.control($('base-url')).getValue().trim(), apiKey: key.disabled ? '' : block.parts.control(key).getValue().trim() }; };
103
+ const update = (next) => {
104
+ if (destroyed)
105
+ return;
106
+ current = next;
107
+ block.update(next);
108
+ block.parts.action(submit).update({ disabled: next.disabled, pending: pending || removing || !!next.testing || next.ready !== true });
109
+ block.parts.action(test).update({ label: next.testing ? 'Cancel test' : 'Test connection', disabled: !!next.disabled || pending || removing, pending: next.ready !== true });
110
+ block.parts.action(remove).update({ disabled: !!next.disabled || pending || !!next.testing || next.ready !== true, pending: removing });
111
+ if (next.appearance) {
112
+ const appearance = block.parts.control($('appearance-mode'));
113
+ appearance.reset(next.appearance.mode);
114
+ appearance.update({ disabled: !!next.appearance.disabled });
115
+ block.parts.feedback($('appearance-error')).update({ message: next.appearance.error || '', tone: 'error' });
116
+ }
117
+ if (next.testResult) {
118
+ const result = $('connection-test-result');
119
+ result.textContent = next.testResult.message;
120
+ result.className = next.testResult.ok === undefined ? '' : next.testResult.ok ? 'pui-test-success' : 'pui-form-error';
121
+ }
122
+ };
123
+ const operation = createOperation({ unavailable: () => !!current.disabled || !!current.testing || current.ready !== true,
124
+ onPending(value) { pending = value && operationKind === 'submit'; removing = value && operationKind === 'remove'; update(current); },
125
+ onError(error) { update({ ...current, error }); } });
126
+ const run = (action, kind) => {
127
+ if (!action || operation.pending || current.disabled || current.testing || current.ready !== true)
128
+ return;
129
+ if (kind === 'submit' && !form.reportValidity())
130
+ return;
131
+ operationKind = kind;
132
+ operation.run(() => { update({ ...current, error: '' }); return action(read()); }, result => { if (!result.ok)
133
+ update({ ...current, error: result.error }); });
134
+ };
135
+ form.addEventListener('submit', event => { if (options.onSubmit) {
136
+ event.preventDefault();
137
+ void run(options.onSubmit, 'submit');
138
+ } }, { signal: abort.signal });
139
+ remove.addEventListener('click', () => { void run(options.onRemoveKey, 'remove'); }, { signal: abort.signal });
140
+ test.addEventListener('click', () => { if (!test.disabled && (current.testing || form.reportValidity()))
141
+ options.onTest?.(read()); }, { signal: abort.signal });
142
+ $('appearance-mode').addEventListener('change', () => options.onAppearanceChange?.($('appearance-mode').value), { signal: abort.signal });
143
+ $('settings-provider').addEventListener('change', () => options.onProviderChange?.(read().provider), { signal: abort.signal });
144
+ form.addEventListener('input', () => options.onDraftChange?.(read()), { signal: abort.signal });
145
+ $('base-url').addEventListener('invalid', () => { $('provider-advanced').open = true; }, { signal: abort.signal });
146
+ update(state);
147
+ return { ...block, read, update, updates: $('update-settings'),
148
+ setProviders(providers, selected) { if (destroyed)
149
+ return; const select = $('settings-provider'), previous = selected ?? select.value; reconcileSelectOptions(select, providers.map(provider => ({ value: provider.id, label: `${provider.name}${provider.signedIn ? ' · Signed in' : provider.configured ? ' · Configured' : ''}` })), providers.some(provider => provider.id === previous) ? previous : providers[0]?.id || ''); block.parts.control(select).update({ disabled: !providers.length }); },
150
+ resetProvider(value) { if (destroyed)
151
+ return; operation.invalidate(); update({ ...current, error: '' }); block.parts.control($('settings-provider')).reset(value.provider); block.parts.control($('base-url')).reset(value.baseUrl); if (value.clearKey)
152
+ block.parts.control($('api-key')).reset(''); if (value.collapseAdvanced)
153
+ $('provider-advanced').open = false; },
154
+ setCredential(view) { if (destroyed)
155
+ return; $('key-status').textContent = view.status; $('provider-api-key').hidden = view.hidden; block.parts.control($('api-key')).update({ disabled: view.hidden, required: view.required, placeholder: view.placeholder }); remove.hidden = !view.removeVisible; $('provider-auth-hint').hidden = !view.hint; $('provider-auth-hint').textContent = view.hint || ''; const actions = $('provider-actions'); actions.hidden = !view.actions; if (view.actions && actions.firstChild !== view.actions)
156
+ actions.replaceChildren(view.actions);
157
+ else if (!view.actions)
158
+ actions.replaceChildren(); block.parts.update(); },
159
+ clearSecrets() { block.parts.control($('api-key')).reset(''); },
160
+ destroy() { if (destroyed)
161
+ return; destroyed = true; operation.destroy(); abort.abort(); $('api-key').value = ''; block.destroy(); }
162
+ };
88
163
  }
89
164
  export function createMCPConnectionsPanel(state = {}, options = {}) {
90
- const block = formBlock(`${dialogHeading('Connections', 'connections-title', 'connections-dialog', 'EXTERNAL TOOLS')}<p class="pui-modal-description">Add an MCP server to give Pith access to its tools. Enabled connections connect automatically when a task starts. Disable a connection to stop using it in tasks. External tools require approval unless this conversation has full access.</p><div class="pui-feature-toolbar"><button class="pui-secondary-button" data-action="refresh-mcp">Refresh</button></div><div id="mcp-list"><p class="pui-feature-hint">Loading connections…</p></div><div id="mcp-error" data-dialog-error class="pui-form-error" role="alert"></div><section class="pui-connection-editor"><div class="pui-section-heading"><span id="mcp-form-title">Add connection</span><button type="button" class="pui-secondary-button" data-action="new-mcp">New connection</button></div><form id="mcp-form"><label class="pui-field-label" for="mcp-name">Name</label><input id="mcp-name" name="name" required autocomplete="off" placeholder="my-tools" /><label class="pui-field-label" for="mcp-type">Connection type</label><select id="mcp-type" name="type" class="pui-feature-select"><option value="http">HTTP</option><option value="stdio">Local command (stdio)</option></select><div id="mcp-http-fields"><label class="pui-field-label" for="mcp-url">Server URL</label><input id="mcp-url" name="url" type="url" placeholder="https://example.com/mcp" autocomplete="off" /><label class="pui-checkbox-field"><input id="mcp-oauth" type="checkbox" />Sign in with OAuth</label><details class="pui-provider-advanced"><summary>OAuth settings (optional)</summary><label class="pui-field-label">Registered client ID<input id="mcp-oauth-client" /></label><label class="pui-field-label">Scopes<input id="mcp-oauth-scope" /></label><p class="pui-field-hint">Uses the server’s OAuth discovery and PKCE. The temporary callback listens on 127.0.0.1:54819.</p></details><label class="pui-field-label" for="mcp-token">Bearer token (optional)</label><input id="mcp-token" name="bearerToken" type="password" autocomplete="new-password" placeholder="Optional token" /><label class="pui-checkbox-field" id="mcp-clear-token-row" hidden><input id="mcp-clear-token" type="checkbox" />Remove saved bearer token</label><p class="pui-field-hint">Saved tokens are never returned to this page. Leave this field blank to keep an existing token.</p></div><div id="mcp-stdio-fields" hidden><label class="pui-field-label" for="mcp-command">Installed executable</label><input id="mcp-command" name="command" placeholder="/path/to/mcp-server" autocomplete="off" /><label class="pui-field-label" for="mcp-args">Arguments (JSON array)</label><textarea id="mcp-args" name="args" rows="3" spellcheck="false">[]</textarea><p class="pui-field-hint">The executable and any runtime it needs must already be installed on your computer. Pith Desk does not bundle external runtimes or a marketplace.</p><div class="pui-secret-label"><label class="pui-field-label" for="mcp-env">Environment overrides (JSON object, optional)</label><button class="pui-secondary-button" type="button" id="mcp-env-visibility" data-action="toggle-env" aria-controls="mcp-env" aria-pressed="false">Show</button></div><textarea id="mcp-env" class="pui-secret-field" name="env" rows="3" spellcheck="false" autocomplete="off" aria-describedby="mcp-env-hint" placeholder='{"API_KEY":"…"}'></textarea><p id="mcp-env-hint" class="pui-field-hint">Values stay private. Leave blank to keep saved overrides. Add a JSON object of string keys and values to set or replace individual overrides.</p><p id="mcp-env-keys" class="pui-field-hint" hidden></p><label class="pui-checkbox-field" id="mcp-clear-env-row" hidden><input id="mcp-clear-env" type="checkbox" />Clear saved environment overrides</label></div><label class="pui-checkbox-field"><input id="mcp-enabled" name="enabled" type="checkbox" checked />Enable this connection</label><div id="mcp-form-error" class="pui-form-error" role="alert"></div><div class="pui-modal-footer"><button class="pui-secondary-button" type="button" data-close="connections-dialog">Close</button><button class="pui-primary-button" type="submit" data-idle-action data-global-idle>Save connection</button></div></form></section>`, 'pui-mcp-connections', state, options.idPrefix ?? uniqueId('pui-form') + '-');
165
+ const block = formBlock(`${dialogHeading('Connections', 'connections-title', 'connections-dialog', 'EXTERNAL TOOLS')}<p class="pui-modal-description">Add an MCP server to give Pith access to its tools. Enabled connections connect automatically when a task starts. Disable a connection to stop using it in tasks. External tools require approval unless this conversation has full access.</p><div class="pui-feature-toolbar"><button type="button" class="pui-secondary-button" data-action="refresh-mcp">Refresh</button></div><div id="mcp-list"><p class="pui-feature-hint">Loading connections…</p></div><div id="mcp-error" data-dialog-error class="pui-form-error" role="alert"></div><section class="pui-connection-editor"><div class="pui-section-heading"><span id="mcp-form-title">Add connection</span><button type="button" class="pui-secondary-button" data-action="new-mcp">New connection</button></div><form id="mcp-form"><label class="pui-field-label" for="mcp-name">Name</label><input id="mcp-name" name="name" required autocomplete="off" placeholder="my-tools" /><label class="pui-field-label" for="mcp-type">Connection type</label><select id="mcp-type" name="type" class="pui-feature-select"><option value="http">HTTP</option><option value="stdio">Local command (stdio)</option></select><div id="mcp-http-fields"><label class="pui-field-label" for="mcp-url">Server URL</label><input id="mcp-url" name="url" type="url" placeholder="https://example.com/mcp" autocomplete="off" /><label class="pui-checkbox-field"><input id="mcp-oauth" type="checkbox" />Sign in with OAuth</label><details class="pui-provider-advanced"><summary>OAuth settings (optional)</summary><label class="pui-field-label">Registered client ID<input id="mcp-oauth-client" /></label><label class="pui-field-label">Scopes<input id="mcp-oauth-scope" /></label><p class="pui-field-hint">Uses the server’s OAuth discovery and PKCE. The temporary callback listens on 127.0.0.1:54819.</p></details><label class="pui-field-label" for="mcp-token">Bearer token (optional)</label><input id="mcp-token" name="bearerToken" type="password" autocomplete="new-password" placeholder="Optional token" /><label class="pui-checkbox-field" id="mcp-clear-token-row" hidden><input id="mcp-clear-token" type="checkbox" />Remove saved bearer token</label><p class="pui-field-hint">Saved tokens are never returned to this page. Leave this field blank to keep an existing token.</p></div><div id="mcp-stdio-fields" hidden><label class="pui-field-label" for="mcp-command">Installed executable</label><input id="mcp-command" name="command" placeholder="/path/to/mcp-server" autocomplete="off" /><label class="pui-field-label" for="mcp-args">Arguments (JSON array)</label><textarea id="mcp-args" name="args" rows="3" spellcheck="false">[]</textarea><p class="pui-field-hint">The executable and any runtime it needs must already be installed on your computer. Pith Desk does not bundle external runtimes or a marketplace.</p><div class="pui-secret-label"><label class="pui-field-label" for="mcp-env">Environment overrides (JSON object, optional)</label><button class="pui-secondary-button" type="button" id="mcp-env-visibility" data-action="toggle-env" aria-controls="mcp-env" aria-pressed="false">Show</button></div><textarea id="mcp-env" class="pui-secret-field" name="env" rows="3" spellcheck="false" autocomplete="off" aria-describedby="mcp-env-hint" placeholder='{"API_KEY":"…"}'></textarea><p id="mcp-env-hint" class="pui-field-hint">Values stay private. Leave blank to keep saved overrides. Add a JSON object of string keys and values to set or replace individual overrides.</p><p id="mcp-env-keys" class="pui-field-hint" hidden></p><label class="pui-checkbox-field" id="mcp-clear-env-row" hidden><input id="mcp-clear-env" type="checkbox" />Clear saved environment overrides</label></div><label class="pui-checkbox-field"><input id="mcp-enabled" name="enabled" type="checkbox" checked />Enable this connection</label><div id="mcp-form-error" class="pui-form-error" role="alert"></div><div class="pui-modal-footer"><button class="pui-secondary-button" type="button" data-pui-close data-close="connections-dialog">Close</button><button class="pui-primary-button" type="submit" data-idle-action data-global-idle>Save connection</button></div></form></section>`, 'pui-mcp-connections', state, options.idPrefix ?? uniqueId('pui-form') + '-');
91
166
  const $ = block.field;
92
167
  const abort = new AbortController();
93
- let disposed = false;
168
+ let disposed = false, pending = false, current = state;
169
+ const form = $('mcp-form'), submit = form.querySelector('button[type="submit"]');
170
+ const control = (id) => block.parts.control($(id));
171
+ const read = () => ({ name: control('mcp-name').getValue().trim(), type: control('mcp-type').getValue() === 'stdio' ? 'stdio' : 'http', url: control('mcp-url').getValue().trim(), oauth: control('mcp-oauth').getChecked(), oauthClientId: control('mcp-oauth-client').getValue().trim(), oauthScope: control('mcp-oauth-scope').getValue().trim(), command: control('mcp-command').getValue().trim(), args: control('mcp-args').getValue(), enabled: control('mcp-enabled').getChecked(), bearerToken: control('mcp-token').getValue().trim(), clearBearerToken: control('mcp-clear-token').getChecked(), env: control('mcp-env').getValue(), clearEnv: control('mcp-clear-env').getChecked() });
172
+ const update = (next) => { if (disposed)
173
+ return; current = next; block.update(next); if (next.formError !== undefined)
174
+ block.parts.feedback($('mcp-form-error')).update({ message: next.formError, tone: 'error' }); block.parts.action(submit).update({ disabled: next.disabled, pending }); };
94
175
  const renderType = () => { if (disposed)
95
- return; const http = $('mcp-type').value === 'http'; $('mcp-http-fields').hidden = !http; $('mcp-stdio-fields').hidden = http; $('mcp-url').required = http; $('mcp-command').required = !http; };
176
+ return; const http = control('mcp-type').getValue() === 'http'; for (const [id, active] of [['mcp-http-fields', http], ['mcp-stdio-fields', !http]]) {
177
+ const group = $(id);
178
+ group.hidden = !active;
179
+ for (const field of group.querySelectorAll('input,textarea'))
180
+ block.parts.control(field).update({ disabled: !active });
181
+ } control('mcp-url').update({ required: http }); control('mcp-command').update({ required: !http }); };
96
182
  const edit = (entry) => {
97
183
  if (disposed)
98
184
  return;
185
+ submission.invalidate();
99
186
  $('mcp-form').reset();
100
187
  $('mcp-env').classList.remove('pui-revealed');
101
188
  $('mcp-env-visibility').setAttribute('aria-pressed', 'false');
102
189
  $('mcp-env-visibility').textContent = 'Show';
103
190
  $('mcp-form-title').textContent = entry ? `Edit ${entry.name}` : 'Add connection';
104
- $('mcp-name').value = entry?.name || '';
105
- $('mcp-name').readOnly = !!entry;
106
- $('mcp-type').value = entry?.type || 'http';
107
- $('mcp-url').value = entry?.url || '';
108
- $('mcp-oauth').checked = entry?.oauth || false;
109
- $('mcp-oauth-client').value = entry?.oauthClientId || '';
110
- $('mcp-oauth-scope').value = entry?.oauthScope || '';
111
- $('mcp-command').value = entry?.command || '';
112
- $('mcp-args').value = JSON.stringify(entry?.args || [], null, 2);
113
- $('mcp-enabled').checked = entry?.enabled ?? true;
114
- $('mcp-token').placeholder = entry?.hasBearerToken ? 'Leave blank to keep your saved token' : 'Optional token';
191
+ control('mcp-name').reset(entry?.name || '');
192
+ control('mcp-name').update({ readOnly: !!entry });
193
+ control('mcp-type').reset(entry?.type || 'http');
194
+ control('mcp-url').reset(entry?.url || '');
195
+ control('mcp-oauth').reset(entry?.oauth || false);
196
+ control('mcp-oauth-client').reset(entry?.oauthClientId || '');
197
+ control('mcp-oauth-scope').reset(entry?.oauthScope || '');
198
+ control('mcp-command').reset(entry?.command || '');
199
+ control('mcp-args').reset(JSON.stringify(entry?.args || [], null, 2));
200
+ control('mcp-enabled').reset(entry?.enabled ?? true);
201
+ control('mcp-token').update({ placeholder: entry?.hasBearerToken ? 'Leave blank to keep your saved token' : 'Optional token' });
115
202
  $('mcp-clear-token-row').hidden = !entry?.hasBearerToken;
116
203
  const envKeys = entry?.envKeys || [];
117
204
  $('mcp-clear-env-row').hidden = !envKeys.length;
118
205
  $('mcp-env-keys').hidden = !envKeys.length;
119
206
  $('mcp-env-keys').textContent = `Saved environment keys: ${envKeys.join(', ')}`;
120
207
  $('mcp-form-error').textContent = '';
208
+ current = { ...current, formError: '' };
209
+ update(current);
121
210
  renderType();
122
211
  $(entry ? entry.type === 'stdio' ? 'mcp-command' : 'mcp-url' : 'mcp-name').focus();
123
212
  };
124
213
  $('mcp-type').addEventListener('change', renderType, { signal: abort.signal });
125
- $('mcp-env-visibility').addEventListener('click', (event) => { event.stopPropagation(); const field = $('mcp-env'); const revealed = field.classList.toggle('pui-revealed'); const button = $('mcp-env-visibility'); button.setAttribute('aria-pressed', String(revealed)); button.textContent = revealed ? 'Hide' : 'Show'; }, { signal: abort.signal });
214
+ const toggleEnvironment = () => { if (disposed)
215
+ return; const field = $('mcp-env'); const revealed = field.classList.toggle('pui-revealed'); const button = $('mcp-env-visibility'); button.setAttribute('aria-pressed', String(revealed)); block.parts.action(button).update({ label: revealed ? 'Hide' : 'Show' }); };
216
+ $('mcp-env-visibility').addEventListener('click', event => { event.stopPropagation(); toggleEnvironment(); }, { signal: abort.signal });
217
+ const submission = bindFormSubmission(form, { read, unavailable: () => !!current.disabled, onSubmit: options.onSubmit, onPending(value) { pending = value; update(current); }, onError(formError) { update({ ...current, formError }); } });
218
+ const clearBearerToken = () => { control('mcp-token').reset(''); };
219
+ const clearSecrets = () => { clearBearerToken(); control('mcp-env').reset(''); };
126
220
  renderType();
127
- return { ...block, renderType, edit, destroy() { if (disposed)
128
- return; disposed = true; abort.abort(); block.destroy(); } };
221
+ update(state);
222
+ return { ...block, update, renderType, edit, read, clearBearerToken, clearSecrets, toggleEnvironment, list: $('mcp-list'), destroy() { if (disposed)
223
+ return; disposed = true; submission.destroy(); abort.abort(); clearSecrets(); block.destroy(); } };
129
224
  }
130
- export function modelEditorMarkup(prefix) { prefix = escape(prefix); return `<label class="pui-field-label" for="${prefix}-provider">Provider</label><select class="pui-feature-select" id="${prefix}-provider" required disabled><option value="">Loading providers…</option></select><label class="pui-field-label" for="${prefix}-search">Model</label><input id="${prefix}-search" type="search" placeholder="Search by model name or ID" autocomplete="off" /><label class="pui-sr-only" for="${prefix}-model">Choose model</label><select id="${prefix}-model" class="pui-model-list" size="6" required disabled></select><p id="${prefix}-capabilities" class="pui-field-hint" aria-live="polite"></p><label class="pui-field-label" id="${prefix}-thinking-label" for="${prefix}-thinking">Thinking effort</label><select id="${prefix}-thinking" class="pui-feature-select" aria-describedby="${prefix}-capabilities" required></select><p class="pui-field-hint">Built-in models come from the Pith SDK catalog. Custom models use your saved definitions. Availability depends on your endpoint and account.</p>`; }
131
225
  export function createModelPicker(state = {}, options = {}) {
132
- return formBlock(`${dialogHeading('Choose a model', 'model-title', 'model-dialog', 'PITH MODEL CATALOG')}<form id="model-form">${modelEditorMarkup('picker')}<div id="model-error" class="pui-form-error" role="alert"></div><div class="pui-modal-footer"><button type="button" class="pui-secondary-button" data-close="model-dialog">Cancel</button><button type="submit" class="pui-primary-button" id="choose-model" data-global-idle data-control-pending="true" disabled>Use model</button></div></form>`, 'pui-model-picker', state, options.idPrefix ?? uniqueId('pui-form') + '-');
226
+ const prefix = options.idPrefix ?? uniqueId('pui-form') + '-';
227
+ const block = formBlock(`${dialogHeading('Choose a model', 'model-title', 'model-dialog', 'PITH MODEL CATALOG')}<form id="model-form">${modelEditorMarkup('picker')}<div id="model-error" class="pui-form-error" role="alert"></div><div class="pui-modal-footer"><button type="button" class="pui-secondary-button" data-pui-close data-close="model-dialog">Cancel</button><button type="submit" class="pui-primary-button" id="choose-model" data-global-idle data-control-pending="true" disabled>Use model</button></div></form>`, 'pui-model-picker', state, prefix);
228
+ let editor;
229
+ let destroyed = false, pending = false, current = state;
230
+ const form = block.field('model-form'), submit = block.field('choose-model');
231
+ const update = (next) => { if (destroyed)
232
+ return; current = next; block.update(next); if (next.editor) {
233
+ if (editor)
234
+ editor.update(next.editor);
235
+ else
236
+ editor = createModelEditor(next.editor, { prefix: prefix + 'picker', element: block.element });
237
+ } const loading = next.loading ?? !next.editor; block.parts.action(submit).update({ disabled: next.disabled, pending: pending || loading }); };
238
+ const read = () => editor?.read() || { provider: '', model: '', thinkingLevel: '' };
239
+ const submission = bindFormSubmission(form, { read, unavailable: () => submit.disabled, onSubmit: options.onSubmit, onPending(value) { pending = value; update(current); }, onError(error) { update({ ...current, error }); } });
240
+ update(state);
241
+ return { ...block, update, read, reset(selection) { submission.invalidate(); editor?.reset(selection); }, focus() { editor?.focus(); }, destroy() { if (destroyed)
242
+ return; destroyed = true; submission.destroy(); editor?.destroy(); block.destroy(); } };
133
243
  }
134
- export function createMCPConnectionList(initial) {
244
+ export function createMCPConnectionList(initial, options = {}) {
135
245
  const element = document.createElement('div');
136
246
  element.className = 'pui-mcp-connection-list';
247
+ const parts = adoptProductParts(element);
137
248
  let destroyed = false;
138
249
  const entries = new Map();
139
250
  const empty = document.createElement('p');
140
251
  empty.className = 'pui-feature-hint';
141
252
  empty.textContent = 'No connections yet. Add an HTTP server or an installed local MCP command below. Tools are discovered on demand using tool search and Codemode.';
142
- function createEntry() {
253
+ function createEntry(item) {
143
254
  const root = document.createElement('article');
144
255
  root.className = 'pui-connection-card';
145
256
  root.innerHTML = '<div class="pui-connection-card-heading"><strong></strong></div><p class="pui-file-path"></p><p class="pui-feature-hint"></p><p class="pui-connection-status" role="status"></p><p class="pui-feature-hint" hidden></p><p class="pui-form-error"></p><div class="pui-file-actions"></div>';
@@ -158,67 +269,62 @@ export function createMCPConnectionList(initial) {
158
269
  node.dataset.idleAction = '';
159
270
  }
160
271
  actions.append(edit, toggle, remove);
161
- return { element: root, name: root.querySelector('strong'), address: paragraphs[0], enabled: paragraphs[1], status: paragraphs[2], env: paragraphs[3], error: paragraphs[4], actions, signIn, signOut, edit, toggle, remove };
272
+ const entry = { element: root, name: root.querySelector('strong'), address: paragraphs[0], enabled: paragraphs[1], status: paragraphs[2], env: paragraphs[3], error: paragraphs[4], actions, signIn, signOut, edit, toggle, remove, handles: new Map(), abort: new AbortController(), localError: '' };
273
+ const target = Object.freeze({ name: item.name });
274
+ for (const [node, action] of [[signIn, 'mcp-login'], [signOut, 'mcp-logout'], [edit, 'mcp-edit'], [toggle, 'mcp-toggle'], [remove, 'mcp-remove']]) {
275
+ if (options.onAction)
276
+ node.addEventListener('click', event => event.stopPropagation(), { signal: entry.abort.signal });
277
+ entry.handles.set(node, adoptAction(node, { onAction: options.onAction ? () => { entry.localError = ''; parts.feedback(entry.error).update({ message: '', tone: 'error' }); return options.onAction(action, target); } : undefined, onError(message) { entry.localError = message; parts.feedback(entry.error).update({ message, tone: 'error' }); } }));
278
+ }
279
+ return entry;
162
280
  }
281
+ const release = (entry) => { entry.abort.abort(); for (const handle of entry.handles.values())
282
+ handle.destroy(); };
163
283
  const update = (state) => {
164
284
  if (destroyed)
165
285
  return;
166
- const current = new Set();
167
- let cursor = element.firstElementChild;
168
- for (const item of state.entries) {
169
- current.add(item.name);
170
- let entry = entries.get(item.name);
171
- if (!entry) {
172
- entry = createEntry();
173
- entries.set(item.name, entry);
174
- }
175
- setText(entry.name, item.name);
176
- entry.element.dataset.connectionKey = item.name;
177
- setText(entry.address, item.type === 'http' ? item.url || '' : [item.command, ...(item.args || [])].join(' '));
178
- setText(entry.enabled, `${item.enabled ? 'Enabled' : 'Disabled'}${item.hasBearerToken ? ' · Token configured' : ''}`);
179
- setText(entry.status, item.statusLabel);
180
- entry.status.classList.toggle('pui-is-error', item.enabled && item.status === 'error');
181
- entry.env.hidden = !item.envKeys?.length;
182
- setText(entry.env, item.envKeys?.length ? 'Saved environment keys: ' + item.envKeys.join(', ') : '');
183
- setText(entry.error, item.error || '');
184
- setText(entry.signIn, item.signedIn ? 'Sign in again' : 'Sign in');
185
- entry.signIn.dataset.id = entry.signOut.dataset.id = item.name;
186
- entry.edit.dataset.mcpEdit = item.name;
187
- entry.toggle.dataset.mcpToggle = item.name;
188
- entry.remove.dataset.mcpRemove = item.name;
189
- setText(entry.toggle, item.enabled ? 'Disable' : 'Enable');
190
- if (item.oauth) {
191
- if (entry.signIn.parentElement !== entry.actions)
192
- entry.actions.insertBefore(entry.signIn, entry.edit);
193
- }
194
- else
195
- entry.signIn.remove();
196
- if (item.oauth && item.signedIn) {
197
- if (entry.signOut.parentElement !== entry.actions)
198
- entry.actions.insertBefore(entry.signOut, entry.edit);
199
- }
200
- else
201
- entry.signOut.remove();
202
- for (const button of [entry.signIn, entry.signOut, entry.edit, entry.toggle, entry.remove])
203
- button.disabled = !!state.disabled;
204
- if (entry.element !== cursor)
205
- element.insertBefore(entry.element, cursor);
206
- cursor = entry.element.nextElementSibling;
207
- }
208
- for (const [key, entry] of entries)
209
- if (!current.has(key)) {
210
- entry.element.remove();
211
- entries.delete(key);
212
- }
286
+ reconcileKeyed(element, entries, state.entries, { key: item => item.name, element: entry => entry.element, create: createEntry, update(entry, item) {
287
+ setText(entry.name, item.name);
288
+ entry.element.dataset.connectionKey = item.name;
289
+ setText(entry.address, item.type === 'http' ? item.url || '' : [item.command, ...(item.args || [])].join(' '));
290
+ setText(entry.enabled, `${item.enabled ? 'Enabled' : 'Disabled'}${item.hasBearerToken ? ' · Token configured' : ''}`);
291
+ setText(entry.status, item.statusLabel);
292
+ entry.status.classList.toggle('pui-is-error', item.enabled && item.status === 'error');
293
+ entry.env.hidden = !item.envKeys?.length;
294
+ setText(entry.env, item.envKeys?.length ? 'Saved environment keys: ' + item.envKeys.join(', ') : '');
295
+ parts.feedback(entry.error).update({ message: item.error || entry.localError, tone: 'error' });
296
+ setText(entry.signIn, item.signedIn ? 'Sign in again' : 'Sign in');
297
+ entry.signIn.dataset.id = entry.signOut.dataset.id = item.name;
298
+ entry.edit.dataset.mcpEdit = item.name;
299
+ entry.toggle.dataset.mcpToggle = item.name;
300
+ entry.remove.dataset.mcpRemove = item.name;
301
+ setText(entry.toggle, item.enabled ? 'Disable' : 'Enable');
302
+ if (item.oauth) {
303
+ if (entry.signIn.parentElement !== entry.actions)
304
+ entry.actions.insertBefore(entry.signIn, entry.edit);
305
+ }
306
+ else
307
+ entry.signIn.remove();
308
+ if (item.oauth && item.signedIn) {
309
+ if (entry.signOut.parentElement !== entry.actions)
310
+ entry.actions.insertBefore(entry.signOut, entry.edit);
311
+ }
312
+ else
313
+ entry.signOut.remove();
314
+ for (const [button, handle] of entry.handles)
315
+ handle.update({ disabled: !!state.disabled || (button === entry.signIn && !item.oauth) || (button === entry.signOut && (!item.oauth || !item.signedIn)) });
316
+ }, destroy: release });
213
317
  if (!state.entries.length) {
214
318
  if (empty.parentElement !== element)
215
319
  element.append(empty);
216
320
  }
217
321
  else
218
322
  empty.remove();
323
+ parts.update();
219
324
  };
220
325
  update(initial);
221
326
  return { element, update, destroy() { if (destroyed)
222
- return; destroyed = true; entries.clear(); element.remove(); } };
327
+ return; destroyed = true; for (const entry of entries.values())
328
+ release(entry); parts.destroy(); entries.clear(); element.remove(); } };
223
329
  }
224
330
  //# sourceMappingURL=panels.js.map