@timqi/pier 0.1.14 → 0.2.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.
- package/README.md +44 -22
- package/dist/agent/config.js +2 -2
- package/dist/agent/events.js +32 -3
- package/dist/agent/packages.js +6 -10
- package/dist/agent/pi.js +111 -68
- package/dist/agent/roles.js +103 -0
- package/dist/agent/types.js +78 -0
- package/dist/boards/boards.js +42 -32
- package/dist/channels/chains.js +0 -4
- package/dist/channels/commands.js +7 -12
- package/dist/channels/config.js +27 -1
- package/dist/channels/control.js +5 -16
- package/dist/channels/conversations.js +0 -5
- package/dist/channels/lark-api.js +20 -26
- package/dist/channels/lark-outbound.js +16 -29
- package/dist/channels/lark-panel.js +1 -1
- package/dist/channels/lark.js +61 -39
- package/dist/channels/lines.js +33 -1
- package/dist/channels/panel.js +17 -107
- package/dist/channels/routes.js +11 -21
- package/dist/channels/runtime.js +34 -25
- package/dist/channels/slack-outbound.js +6 -15
- package/dist/channels/slack-panel.js +1 -1
- package/dist/channels/slack-render.js +1 -1
- package/dist/channels/slack.js +63 -57
- package/dist/channels/types.js +1 -1
- package/dist/config-sync.js +42 -37
- package/dist/core/chain.js +202 -0
- package/dist/core/identity.js +34 -11
- package/dist/core/inbound-file.js +4 -3
- package/dist/core/reply.js +96 -80
- package/dist/core/router.js +111 -118
- package/dist/core/types.js +36 -77
- package/dist/db.js +33 -2
- package/dist/main.js +46 -37
- package/dist/settings.js +25 -18
- package/dist/tasks/agent.js +11 -5
- package/dist/tasks/callbacks.js +55 -5
- package/dist/tasks/cli.js +22 -11
- package/dist/tasks/definitions.js +30 -17
- package/dist/tasks/execution.js +2 -2
- package/dist/tasks/groups.js +5 -2
- package/dist/tasks/messages.js +7 -7
- package/dist/tasks/open-items.js +70 -0
- package/dist/tasks/operations.js +76 -39
- package/dist/tasks/outbox.js +19 -6
- package/dist/tasks/routes.js +19 -249
- package/dist/tasks/service.js +145 -34
- package/dist/tasks/store.js +119 -56
- package/dist/tasks/types.js +5 -0
- package/dist/tools.js +2 -2
- package/dist/web/auth.js +2 -2
- package/dist/web/config-sync.js +4 -5
- package/dist/web/config.js +9 -2
- package/dist/web/explorer.js +2 -2
- package/dist/web/fs.js +16 -1
- package/dist/web/instance.js +8 -5
- package/dist/web/packages.js +1 -1
- package/dist/web/passkeys.js +3 -3
- package/dist/web/providers.js +3 -3
- package/dist/web/public/assets/code-BZwvPani.js +2 -0
- package/dist/web/public/assets/code-BZwvPani.js.br +0 -0
- package/dist/web/public/assets/code-BZwvPani.js.gz +0 -0
- package/dist/web/public/assets/explorer-DDoVzMLQ.js +5 -0
- package/dist/web/public/assets/explorer-DDoVzMLQ.js.br +0 -0
- package/dist/web/public/assets/explorer-DDoVzMLQ.js.gz +0 -0
- package/dist/web/public/assets/highlight-5Bl8zZ8q.js +72 -0
- package/dist/web/public/assets/highlight-5Bl8zZ8q.js.br +0 -0
- package/dist/web/public/assets/highlight-5Bl8zZ8q.js.gz +0 -0
- package/dist/web/public/assets/{hljs-tWqyD59G.js → hljs-BRz6a8Dq.js} +2 -2
- package/dist/web/public/assets/hljs-BRz6a8Dq.js.br +0 -0
- package/dist/web/public/assets/hljs-BRz6a8Dq.js.gz +0 -0
- package/dist/web/public/assets/index-BpFzCwlo.js +7 -0
- package/dist/web/public/assets/index-BpFzCwlo.js.br +0 -0
- package/dist/web/public/assets/index-BpFzCwlo.js.gz +0 -0
- package/dist/web/public/assets/index-wAGxFFPj.css +2 -0
- package/dist/web/public/assets/index-wAGxFFPj.css.br +0 -0
- package/dist/web/public/assets/index-wAGxFFPj.css.gz +0 -0
- package/dist/web/public/assets/report-zjE8Bt8S.js +8 -0
- package/dist/web/public/assets/report-zjE8Bt8S.js.br +0 -0
- package/dist/web/public/assets/report-zjE8Bt8S.js.gz +0 -0
- package/dist/web/public/assets/settings-Cv7rbeqf.js +9 -0
- package/dist/web/public/assets/settings-Cv7rbeqf.js.br +0 -0
- package/dist/web/public/assets/settings-Cv7rbeqf.js.gz +0 -0
- package/dist/web/public/index.html +40 -109
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/manifest.webmanifest +0 -2
- package/dist/web/push.js +1 -1
- package/dist/web/server.js +91 -116
- package/dist/web/session-state.js +5 -40
- package/docs/deploy.md +5 -3
- package/package.json +2 -1
- package/skills/pier-boards/SKILL.md +14 -4
- package/skills/pier-help/SKILL.md +57 -23
- package/skills/pier-tasks/SKILL.md +58 -18
- package/dist/channels/handoff.js +0 -94
- package/dist/web/public/assets/activity-Ds6fCHnb.js +0 -5
- package/dist/web/public/assets/activity-Ds6fCHnb.js.br +0 -0
- package/dist/web/public/assets/activity-Ds6fCHnb.js.gz +0 -0
- package/dist/web/public/assets/boards-BKCj6EwK.js +0 -1
- package/dist/web/public/assets/boards-BKCj6EwK.js.br +0 -0
- package/dist/web/public/assets/boards-BKCj6EwK.js.gz +0 -0
- package/dist/web/public/assets/explorer-DnTm975c.js +0 -4
- package/dist/web/public/assets/explorer-DnTm975c.js.br +0 -0
- package/dist/web/public/assets/explorer-DnTm975c.js.gz +0 -0
- package/dist/web/public/assets/hljs-tWqyD59G.js.br +0 -0
- package/dist/web/public/assets/hljs-tWqyD59G.js.gz +0 -0
- package/dist/web/public/assets/index-BQo-haPN.js +0 -85
- package/dist/web/public/assets/index-BQo-haPN.js.br +0 -0
- package/dist/web/public/assets/index-BQo-haPN.js.gz +0 -0
- package/dist/web/public/assets/index-DiHj0w1i.css +0 -2
- package/dist/web/public/assets/index-DiHj0w1i.css.br +0 -0
- package/dist/web/public/assets/index-DiHj0w1i.css.gz +0 -0
- package/dist/web/public/assets/runs-C_AthWcW.js +0 -1
- package/dist/web/public/assets/runs-C_AthWcW.js.br +0 -0
- package/dist/web/public/assets/runs-C_AthWcW.js.gz +0 -0
- package/dist/web/public/assets/settings-DDaAtFlc.js +0 -5
- package/dist/web/public/assets/settings-DDaAtFlc.js.br +0 -0
- package/dist/web/public/assets/settings-DDaAtFlc.js.gz +0 -0
- package/dist/web/public/assets/task-runs-0pvdITiV.js +0 -3
- package/dist/web/public/assets/task-runs-0pvdITiV.js.br +0 -0
- package/dist/web/public/assets/task-runs-0pvdITiV.js.gz +0 -0
- package/dist/web/public/assets/tasks-Bz29caHJ.js +0 -4
- package/dist/web/public/assets/tasks-Bz29caHJ.js.br +0 -0
- package/dist/web/public/assets/tasks-Bz29caHJ.js.gz +0 -0
|
@@ -38,115 +38,46 @@
|
|
|
38
38
|
which style.css pays for with the safe-area inset. -->
|
|
39
39
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
40
40
|
<title>Pier</title>
|
|
41
|
-
<script type="module" crossorigin src="/app/assets/index-
|
|
42
|
-
<link rel="
|
|
41
|
+
<script type="module" crossorigin src="/app/assets/index-BpFzCwlo.js"></script>
|
|
42
|
+
<link rel="modulepreload" crossorigin href="/app/assets/highlight-5Bl8zZ8q.js">
|
|
43
|
+
<link rel="modulepreload" crossorigin href="/app/assets/hljs-BRz6a8Dq.js">
|
|
44
|
+
<link rel="modulepreload" crossorigin href="/app/assets/report-zjE8Bt8S.js">
|
|
45
|
+
<link rel="stylesheet" crossorigin href="/app/assets/index-wAGxFFPj.css">
|
|
43
46
|
</head>
|
|
44
47
|
<!-- The document never scrolls: this is a fixed-viewport workbench, and every
|
|
45
48
|
scrollable region is an inner pane. h-dvh, not h-screen, because 100vh can
|
|
46
49
|
exceed the visible viewport (mobile chrome, short windows) — that gap is
|
|
47
|
-
what lets the whole app slide up and take the
|
|
48
|
-
<body class="
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
moves it between the rail's top-right (open) and main's top-left
|
|
54
|
-
(closed), flipping the chevron, so expand and collapse are the same
|
|
55
|
-
control in the same band. -->
|
|
56
|
-
<button type="button" id="rail-toggle" aria-label="Toggle sidebar"
|
|
57
|
-
class="fixed top-1.5 z-40 hidden h-7 w-7 cursor-pointer items-center justify-center rounded-md text-neutral-400 hover:bg-neutral-200 hover:text-neutral-700 md:flex">
|
|
58
|
-
<span data-icon="ChevronLeft" class="h-3.5 w-3.5"></span>
|
|
59
|
-
</button>
|
|
60
|
-
<aside id="sidebar" class="flex flex-col overflow-hidden border-r border-neutral-200 bg-neutral-50 font-sans">
|
|
61
|
-
<header class="flex h-12 flex-none items-center gap-2 px-3">
|
|
62
|
-
<!-- The same mark as the tab and the home-screen icon, so an installed
|
|
63
|
-
Pier looks like the thing you installed. -->
|
|
64
|
-
<img src="/app/icon.svg" alt="" width="20" height="20" class="h-7 w-7 flex-none rounded-lg border border-neutral-200/70 bg-white p-1 shadow-xs" />
|
|
65
|
-
<h1 class="text-[17px] font-semibold tracking-tight">Pier</h1>
|
|
50
|
+
what lets the whole app slide up and take the bar with it. -->
|
|
51
|
+
<body class="h-dvh overflow-hidden text-[16px] leading-[1.45] text-neutral-800 antialiased">
|
|
52
|
+
<main class="h-full min-w-0 overflow-hidden">
|
|
53
|
+
<!-- Settings moves both into its own head (settings.ts); they live here
|
|
54
|
+
until then because version.ts and theme.ts wire them at boot. -->
|
|
55
|
+
<section id="settings-view" class="hidden min-h-0 flex-1 flex-col">
|
|
66
56
|
<!-- A link, not a label: AGPL-3.0 section 13 asks a network service to
|
|
67
|
-
point its users at the source it is running.
|
|
68
|
-
because a version is something you look up, not something you find
|
|
69
|
-
by scrolling to the bottom. -->
|
|
57
|
+
point its users at the source it is running. -->
|
|
70
58
|
<a id="version" href="https://github.com/timqi/pier" target="_blank" rel="noreferrer"
|
|
71
59
|
title="Pier is free software (AGPL-3.0) — the source of what you are using"
|
|
72
|
-
class="font-mono text-[11px] text-neutral-400 hover:text-neutral-700 hover:underline"></a>
|
|
60
|
+
class="flex-none font-mono text-[11px] text-neutral-400 hover:text-neutral-700 hover:underline"></a>
|
|
73
61
|
<!-- Cycles system → light → dark; the glyph is what is on screen, the
|
|
74
|
-
title says which of the three states put it there.
|
|
75
|
-
version rather than at the header's right edge, which is where the
|
|
76
|
-
fixed rail toggle lands. -->
|
|
62
|
+
title says which of the three states put it there. -->
|
|
77
63
|
<button type="button" id="theme-toggle" aria-label="Theme"
|
|
78
64
|
class="icon-btn text-[13px]"></button>
|
|
79
|
-
</header>
|
|
80
|
-
<div class="flex flex-none items-center gap-1.5 px-3 pb-1">
|
|
81
|
-
<button type="button" id="new-session" aria-label="New session"
|
|
82
|
-
class="btn btn-primary flex min-w-0 flex-1 items-center justify-center gap-2 whitespace-nowrap px-3">
|
|
83
|
-
<span data-icon="Plus" aria-hidden="true" class="h-3.5 w-3.5"></span> New session
|
|
84
|
-
</button>
|
|
85
|
-
<button type="button" id="open-palette" aria-label="Search sessions, messages and Console"
|
|
86
|
-
class="btn flex h-8 w-8 flex-none items-center justify-center bg-neutral-100 p-0 text-neutral-600 hover:bg-neutral-200 active:bg-neutral-300">
|
|
87
|
-
<span data-icon="Search" class="h-5 w-5 flex-none"></span>
|
|
88
|
-
<span class="sr-only">Search</span>
|
|
89
|
-
</button>
|
|
90
|
-
</div>
|
|
91
|
-
<details id="console-section" open class="flex-none border-b border-neutral-200/70">
|
|
92
|
-
<summary class="flex cursor-pointer select-none items-center gap-1 px-3 py-1 text-xs font-semibold leading-5 text-neutral-500 hover:bg-neutral-100">
|
|
93
|
-
<span data-icon="ChevronRight" class="chev h-3 w-3"></span>Console
|
|
94
|
-
</summary>
|
|
95
|
-
<ul class="pb-1">
|
|
96
|
-
<!-- One row for the tasks area: Tasks, Runs and Activity are tabs of
|
|
97
|
-
the Automation page (views.ts), not three sidebar entries. -->
|
|
98
|
-
<li><button type="button" id="open-tasks"
|
|
99
|
-
class="flex w-full cursor-pointer items-center gap-2 pr-3 text-left hover:bg-neutral-100">
|
|
100
|
-
<span data-icon="ListTodo" class="h-3.5 w-3.5 flex-none text-neutral-400"></span>Automation</button></li>
|
|
101
|
-
<li><button type="button" id="open-boards"
|
|
102
|
-
class="flex w-full cursor-pointer items-center gap-2 pr-3 text-left hover:bg-neutral-100">
|
|
103
|
-
<span data-icon="LayoutDashboard" class="h-3.5 w-3.5 flex-none text-neutral-400"></span>Boards</button></li>
|
|
104
|
-
<li><button type="button" id="open-settings"
|
|
105
|
-
class="flex w-full cursor-pointer items-center gap-2 pr-3 text-left hover:bg-neutral-100">
|
|
106
|
-
<span data-icon="Settings" class="h-3.5 w-3.5 flex-none text-neutral-400"></span>Settings</button></li>
|
|
107
|
-
</ul>
|
|
108
|
-
</details>
|
|
109
|
-
<div class="flex-none px-3 pb-1 pt-2 text-xs font-semibold leading-5 text-neutral-500">Sessions</div>
|
|
110
|
-
<div id="session-list" class="min-h-0 flex-1 overflow-y-auto"></div>
|
|
111
|
-
</aside>
|
|
112
|
-
|
|
113
|
-
<main class="min-w-0 overflow-hidden">
|
|
114
|
-
<!-- Mobile top bar: replaces the chat header below md (which hides), so a
|
|
115
|
-
Console view keeps the same way back to the drawer as a chat does. -->
|
|
116
|
-
<header id="mobile-bar" class="flex flex-none items-center gap-1 px-1.5 md:hidden">
|
|
117
|
-
<button type="button" id="drawer-toggle" aria-label="Open sidebar"
|
|
118
|
-
class="icon-btn relative h-11 w-11 text-neutral-500">
|
|
119
|
-
<span data-icon="Menu" class="h-4.5 w-4.5"></span>
|
|
120
|
-
</button>
|
|
121
|
-
<span id="mobile-title" class="min-w-0 flex-1 truncate text-[16px] font-medium"></span>
|
|
122
|
-
<!-- Both bar controls are the shared round icon button at a thumb's size
|
|
123
|
-
(44px), with the chat header's own 14px ⋯ glyph — not a row of
|
|
124
|
-
oversized dots. -->
|
|
125
|
-
<button type="button" id="mobile-menu" aria-label="Session actions"
|
|
126
|
-
class="icon-btn hidden h-11 w-11"><span data-icon="Ellipsis" class="h-3.5 w-3.5"></span></button>
|
|
127
|
-
</header>
|
|
128
|
-
<!-- Automation hosts three views under one head — title, tabs and the
|
|
129
|
-
page's primary action on one strip, drawn by views.ts; its active pill
|
|
130
|
-
tracks the route (#/tasks, #/runs, #/activity). -->
|
|
131
|
-
<section id="automation-view" class="hidden min-h-0 flex-1 flex-col">
|
|
132
|
-
<header id="automation-tabs" class="pagehead"></header>
|
|
133
|
-
<section id="tasks-view" class="hidden min-h-0 flex-1 flex-col"></section>
|
|
134
|
-
<section id="runs-view" class="hidden min-h-0 flex-1 flex-col"></section>
|
|
135
|
-
<section id="activity-view" class="hidden min-h-0 flex-1 flex-col"></section>
|
|
136
65
|
</section>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
66
|
+
<!-- The one bar, at every width: the way back, the title (Session info),
|
|
67
|
+
the phase tag, then the status chip (the status panel, drawer.ts),
|
|
68
|
+
the meta chips and ⋯ (session-header.ts). Flat children, so below md the
|
|
69
|
+
urgent meta row can wrap to a line of its own (style.css). -->
|
|
70
|
+
<header id="bar" class="flex flex-none items-center gap-1.5 px-1.5 md:px-3">
|
|
71
|
+
<button type="button" id="bar-back" aria-label="Back to the conversation" title="Back to the conversation"
|
|
72
|
+
class="icon-btn hidden h-11 w-11 gap-0.5 text-neutral-500"></button>
|
|
73
|
+
<button type="button" id="chat-title" class="min-w-0 truncate text-left font-medium"></button>
|
|
74
|
+
<span id="chat-phase" class="mr-auto flex flex-none items-center"></span>
|
|
75
|
+
<button type="button" id="status-chip" title="Status"
|
|
76
|
+
class="hidden max-w-[45vw] flex-none cursor-pointer truncate rounded-full bg-neutral-100 px-2 py-0.5 text-[12px] font-medium text-neutral-600 hover:bg-neutral-200"></button>
|
|
77
|
+
<!-- Inline session meta: model · reasoning · current context size. -->
|
|
78
|
+
<div id="session-meta" class="hidden flex-none items-center gap-1 text-[11px]"></div>
|
|
79
|
+
<button type="button" id="chat-menu" title="Session actions" aria-label="Session actions"
|
|
80
|
+
class="icon-btn"><span data-icon="Ellipsis" class="h-3.5 w-3.5"></span></button>
|
|
150
81
|
</header>
|
|
151
82
|
<!-- The transcript's own type scale, inherited by rows, chips and markdown. -->
|
|
152
83
|
<!-- data-gallery: the lightbox pages within the container it was opened
|
|
@@ -157,7 +88,7 @@
|
|
|
157
88
|
<div id="queue-panel" class="hidden flex-col border-t border-neutral-200 px-4 pb-3 pt-2">
|
|
158
89
|
<div class="flex h-7 items-center gap-2">
|
|
159
90
|
<span id="queue-label" class="text-[11px] font-semibold uppercase tracking-wide text-amber-700">Queued</span>
|
|
160
|
-
<div class="ml-auto flex items-center gap-0.5 text-amber-700">
|
|
91
|
+
<div id="queue-actions" class="ml-auto flex items-center gap-0.5 text-amber-700">
|
|
161
92
|
<button type="button" id="queue-steer" title="Send the whole queue now into the running turn" aria-label="Send queue now"
|
|
162
93
|
class="flex h-7 w-7 cursor-pointer items-center justify-center rounded-md hover:bg-amber-100 active:bg-amber-200">
|
|
163
94
|
<span data-icon="ArrowUp" class="h-3.5 w-3.5"></span>
|
|
@@ -179,6 +110,11 @@
|
|
|
179
110
|
<form id="composer" class="flex-none px-4 pb-4 pt-2">
|
|
180
111
|
<div class="glass rounded-[1.75rem] border border-neutral-200 px-3 py-2.5 transition-shadow focus-within:border-indigo-400">
|
|
181
112
|
<div id="image-strip" data-gallery class="hidden flex-wrap gap-2 px-1 pb-1.5 pt-1"></div>
|
|
113
|
+
<!-- The chat commands and skills, offered while the draft is a `/` prefix
|
|
114
|
+
of one (composer.ts): the input keeps the caret, ↑/↓ walk, Enter or Tab
|
|
115
|
+
picks. Rows are subgrids of its two columns, so the lines align;
|
|
116
|
+
eight rows (36px, 44px on touch: style.css) and the padding, then it scrolls. -->
|
|
117
|
+
<ul id="command-menu" role="listbox" aria-label="Commands and skills" class="hidden max-h-[18.5rem] grid-cols-[auto_minmax(0,1fr)] overflow-y-auto overscroll-contain pb-1.5 pt-0.5"></ul>
|
|
182
118
|
<!-- items-end keeps the icons on the bottom row while the textarea grows upward -->
|
|
183
119
|
<div class="flex items-end gap-1.5">
|
|
184
120
|
<button type="button" id="attach" title="Attach file (or paste / drop)"
|
|
@@ -261,16 +197,11 @@
|
|
|
261
197
|
<button type="button" id="image-close" aria-label="Close" class="lightbox-btn right-2 top-[calc(0.5rem+env(safe-area-inset-top))]"><span data-icon="X" class="h-6 w-6"></span></button>
|
|
262
198
|
</dialog>
|
|
263
199
|
|
|
264
|
-
<!--
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
<a id="file-download" download class="btn flex-none text-[12px] no-underline">Download</a>
|
|
270
|
-
<button type="button" id="file-close" class="btn flex-none text-[12px]">Close</button>
|
|
271
|
-
</div>
|
|
272
|
-
<div id="file-text" class="max-h-[70vh] overflow-auto"></div>
|
|
273
|
-
</dialog>
|
|
200
|
+
<!-- The Files dialog (ui/explorer.ts fills it): a tree beside a viewer, over
|
|
201
|
+
the chat or Settings. Nearly the viewport at every width, and the whole
|
|
202
|
+
screen below md, where it pays the safe-area insets `main` pays. -->
|
|
203
|
+
<dialog id="files-dialog" aria-label="Files"
|
|
204
|
+
class="m-auto h-[calc(100dvh-2rem)] max-h-none w-[90rem] max-w-[calc(100vw-2rem)] flex-col overflow-hidden rounded-3xl border border-neutral-200 bg-(--workbench-canvas) p-0 shadow-xl backdrop:bg-black/40 open:flex max-md:m-0 max-md:h-dvh max-md:w-full max-md:max-w-none max-md:rounded-none max-md:border-0 max-md:pt-[env(safe-area-inset-top)] max-md:pb-[env(safe-area-inset-bottom)]"></dialog>
|
|
274
205
|
|
|
275
206
|
<!-- Before the bundle, and only until it loads: a module that throws while
|
|
276
207
|
evaluating leaves a blank page, no handler installed and nothing in the
|
|
Binary file
|
|
Binary file
|
|
@@ -19,8 +19,6 @@
|
|
|
19
19
|
{ "src": "/app/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
|
20
20
|
],
|
|
21
21
|
"shortcuts": [
|
|
22
|
-
{ "name": "Tasks", "url": "/app/#/tasks", "icons": [{ "src": "/app/icon-192.png", "sizes": "192x192" }] },
|
|
23
|
-
{ "name": "Activity", "url": "/app/#/activity", "icons": [{ "src": "/app/icon-192.png", "sizes": "192x192" }] },
|
|
24
22
|
{ "name": "Settings", "url": "/app/#/settings", "icons": [{ "src": "/app/icon-192.png", "sizes": "192x192" }] }
|
|
25
23
|
]
|
|
26
24
|
}
|
package/dist/web/push.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Notifications for the workbench that is not on screen. The rule is the
|
|
2
|
-
//
|
|
2
|
+
// drawer's unread dot, read a few seconds late: a client with the session
|
|
3
3
|
// visible acks immediately, so "still unread" is precisely "nobody saw it".
|
|
4
4
|
// The wire format is webpush.ts.
|
|
5
5
|
import { sessionLabel } from "../core/identity.js";
|
package/dist/web/server.js
CHANGED
|
@@ -9,15 +9,16 @@ import { serveStatic } from "@hono/node-server/serve-static";
|
|
|
9
9
|
import { Hono } from "hono";
|
|
10
10
|
import { compress } from "hono/compress";
|
|
11
11
|
import { streamSSE } from "hono/streaming";
|
|
12
|
+
import { ChatCommandRefused } from "../core/chain.js";
|
|
12
13
|
import { EventHub } from "../core/hub.js";
|
|
13
14
|
import { logger } from "../log.js";
|
|
14
|
-
import { QueueOperationError, Router } from "../core/router.js";
|
|
15
|
+
import { QueueOperationError, Router, SkillAmbiguous } from "../core/router.js";
|
|
15
16
|
import { registerConfigRoutes } from "./config.js";
|
|
16
17
|
import { registerExplorerRoutes } from "./explorer.js";
|
|
17
18
|
import { registerPackageRoutes } from "./packages.js";
|
|
18
19
|
import { fileHeaders, MAX_FILE_BYTES, registerFsRoutes } from "./fs.js";
|
|
19
20
|
import { guarded } from "./route.js";
|
|
20
|
-
import {
|
|
21
|
+
import { CHAIN_FULL_TOKENS, isThinkingLevel } from "../core/types.js";
|
|
21
22
|
import { saveInbound } from "../core/inbox.js";
|
|
22
23
|
import { MAX_INBOUND_BYTES } from "../core/inbound-file.js";
|
|
23
24
|
import {} from "./session-state.js";
|
|
@@ -31,7 +32,7 @@ async function queueResponse(action, status = 200) {
|
|
|
31
32
|
}
|
|
32
33
|
catch (err) {
|
|
33
34
|
const code = err instanceof QueueOperationError
|
|
34
|
-
? err.reason === "draining" ? 503 :
|
|
35
|
+
? err.reason === "draining" ? 503 : 409
|
|
35
36
|
: 404;
|
|
36
37
|
return Response.json({ error: String(err) }, { status: code });
|
|
37
38
|
}
|
|
@@ -100,7 +101,7 @@ function boundedWriter(stream, who) {
|
|
|
100
101
|
}
|
|
101
102
|
// Canonical base64 only: Buffer.from(.., "base64") happily "decodes" garbage.
|
|
102
103
|
const BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
103
|
-
export function createServer({ factory, router, hub, sessions: state, config, packages, providers, settings, catalog, names, onToolsChanged, validateCustomTools, secrets, passkeys, onUnlocked, reload, updates, updater, backgroundRuns, activeBackgroundRunCounts, taskSessions, channelOf, }) {
|
|
104
|
+
export function createServer({ factory, router, hub, sessions: state, config, packages, providers, settings, catalog, names, onToolsChanged, validateCustomTools, secrets, passkeys, onUnlocked, reload, updates, updater, backgroundRuns, activeBackgroundRunCounts, parkedMessages, subscribed, taskSessions, leads, openItems, channelOf, continuous, }) {
|
|
104
105
|
const app = new Hono();
|
|
105
106
|
const epoch = randomUUID();
|
|
106
107
|
// One frame shared by synchronous fan-out to every watching tab.
|
|
@@ -119,6 +120,10 @@ export function createServer({ factory, router, hub, sessions: state, config, pa
|
|
|
119
120
|
// elsewhere, and nothing could ever clear the mark (the ack needs the session
|
|
120
121
|
// on screen).
|
|
121
122
|
const workbenchOwn = (id) => (channelOf?.(id) ?? "web") === "web" && !(taskSessions?.().has(id) ?? false);
|
|
123
|
+
// Outside the continuous conversation only a turn the operator sent into is
|
|
124
|
+
// theirs to read: a lead's dispatch or callback turn reports through main.
|
|
125
|
+
const operatorSent = new Set();
|
|
126
|
+
const sentByOperator = (id) => void operatorSent.add(id);
|
|
122
127
|
const runningNow = new Set();
|
|
123
128
|
hub.subscribeWorkspace((e) => {
|
|
124
129
|
if (e.type !== "session-state")
|
|
@@ -129,7 +134,8 @@ export function createServer({ factory, router, hub, sessions: state, config, pa
|
|
|
129
134
|
}
|
|
130
135
|
if (!runningNow.delete(e.sessionId))
|
|
131
136
|
return;
|
|
132
|
-
|
|
137
|
+
const theirs = operatorSent.delete(e.sessionId) || continuous.chainOf(e.sessionId) !== undefined;
|
|
138
|
+
if (!theirs || !workbenchOwn(e.sessionId))
|
|
133
139
|
return;
|
|
134
140
|
state.setUnread(e.sessionId, true);
|
|
135
141
|
hub.emitWorkspace({ type: "sessions-changed" });
|
|
@@ -137,25 +143,16 @@ export function createServer({ factory, router, hub, sessions: state, config, pa
|
|
|
137
143
|
/** One query for a whole list, not one per row. */
|
|
138
144
|
const activeRuns = () => activeBackgroundRunCounts?.() ?? new Map();
|
|
139
145
|
const ensure = (id) => router.ensure({ channelId: "web", conversationId: id });
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
catch (err) {
|
|
151
|
-
if (String(err).includes("unknown session")) {
|
|
152
|
-
nascent.delete(id);
|
|
153
|
-
state.forget(id);
|
|
154
|
-
hub.emitWorkspace({ type: "sessions-changed" });
|
|
155
|
-
throw new Error(`session ${id} no longer exists — it never got a first reply, so nothing was persisted; its rail entry was removed`);
|
|
156
|
-
}
|
|
157
|
-
throw err;
|
|
158
|
-
}
|
|
146
|
+
// An earlier member of the continuous conversation is read off disk and
|
|
147
|
+
// never opened, so it can be neither edited nor resumed.
|
|
148
|
+
const older = (id) => (continuous.chainOf(id)?.[0] ?? id) !== id;
|
|
149
|
+
const turnsOf = async (id) => {
|
|
150
|
+
if (!older(id))
|
|
151
|
+
return (await ensure(id)).history();
|
|
152
|
+
const turns = await factory.readHistory(id);
|
|
153
|
+
if (!turns)
|
|
154
|
+
throw new Error(`unknown session: ${id}`);
|
|
155
|
+
return turns;
|
|
159
156
|
};
|
|
160
157
|
// Concurrent consumers share one scan; nothing is cached past the last of them.
|
|
161
158
|
let listing;
|
|
@@ -164,55 +161,34 @@ export function createServer({ factory, router, hub, sessions: state, config, pa
|
|
|
164
161
|
});
|
|
165
162
|
const allSessions = async () => {
|
|
166
163
|
const sessions = await listSessions();
|
|
167
|
-
for (const s of sessions)
|
|
168
|
-
nascent.delete(s.id);
|
|
169
|
-
// A session created but never prompted would otherwise hold a working-set
|
|
170
|
-
// slot forever.
|
|
171
|
-
for (const [id, n] of nascent) {
|
|
172
|
-
if (Date.now() - n.createdAt > 86_400_000) {
|
|
173
|
-
nascent.delete(id);
|
|
174
|
-
state.forget(id);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
164
|
const owned = taskSessions?.() ?? new Set();
|
|
178
|
-
return
|
|
179
|
-
...[...nascent].map(([id, n]) => ({ id, ...n })),
|
|
180
|
-
...sessions.filter((s) => !owned.has(s.id)),
|
|
181
|
-
];
|
|
165
|
+
return sessions.filter((s) => !owned.has(s.id));
|
|
182
166
|
};
|
|
183
|
-
// `
|
|
184
|
-
|
|
185
|
-
const present = (s, own, active) => ({
|
|
167
|
+
// `modified` is for the row's tooltip and orders nothing.
|
|
168
|
+
const leadOf = (lead) => (lead ? { phase: lead.phase, ...(lead.runLive ? { runLive: true } : {}), ...(lead.designOpen ? { designOpen: true } : {}) } : {});
|
|
169
|
+
const present = (s, own, active, lead) => ({
|
|
186
170
|
...s,
|
|
187
|
-
...(own?.rank === undefined ? {} : { rank: own.rank }),
|
|
188
171
|
state: router.stateOf(s.id) ?? "idle",
|
|
189
172
|
unread: own?.unread ?? false,
|
|
190
173
|
channel: channelOf?.(s.id) ?? "web",
|
|
191
174
|
activeRuns: active.get(s.id) ?? 0,
|
|
192
|
-
|
|
193
|
-
// The rail's top rows are maintained here and nowhere else: a session a
|
|
194
|
-
// human speaks to — or creates, below — and that is not in the working set
|
|
195
|
-
// already takes the front slot (web/session-state.ts). No route — there is no
|
|
196
|
-
// gesture to make, and an IM message has no browser to make it from.
|
|
197
|
-
router.onSpokenTo((id) => {
|
|
198
|
-
if (state.promote(id))
|
|
199
|
-
hub.emitWorkspace({ type: "sessions-changed" });
|
|
175
|
+
...leadOf(lead.get(s.id)),
|
|
200
176
|
});
|
|
201
177
|
app.get("/api/sessions", async (c) => {
|
|
202
178
|
const flags = state.flags();
|
|
203
179
|
const active = activeRuns();
|
|
204
|
-
|
|
180
|
+
const lead = leads?.() ?? new Map();
|
|
181
|
+
return c.json((await allSessions()).map((s) => present(s, flags.get(s.id), active, lead)));
|
|
205
182
|
});
|
|
206
183
|
// One session by id, the listing's filters aside: a task run's own session is
|
|
207
|
-
// never a row (allSessions), and the pane that opened it from
|
|
184
|
+
// never a row (allSessions), and the pane that opened it from a run card still has
|
|
208
185
|
// a header to name and a session info panel to fill.
|
|
209
186
|
app.get("/api/sessions/:id", async (c) => {
|
|
210
187
|
const id = c.req.param("id");
|
|
211
|
-
const
|
|
212
|
-
const summary = n ? { id, ...n } : (await listSessions()).find((s) => s.id === id);
|
|
188
|
+
const summary = (await listSessions()).find((s) => s.id === id);
|
|
213
189
|
if (!summary)
|
|
214
190
|
return c.json({ error: `no session ${id}` }, 404);
|
|
215
|
-
return c.json(present(summary, state.flags().get(id), activeRuns()));
|
|
191
|
+
return c.json(present(summary, state.flags().get(id), activeRuns(), leads?.() ?? new Map()));
|
|
216
192
|
});
|
|
217
193
|
// What was said, across every session: the palette's Messages section. The
|
|
218
194
|
// factory owns the index and the ranking; an empty query is an empty answer,
|
|
@@ -221,24 +197,6 @@ export function createServer({ factory, router, hub, sessions: state, config, pa
|
|
|
221
197
|
const q = (c.req.query("q") ?? "").trim();
|
|
222
198
|
return c.json({ hits: q ? await factory.search(q) : [] });
|
|
223
199
|
});
|
|
224
|
-
app.post("/api/sessions", async (c) => {
|
|
225
|
-
const body = await c.req.json().catch(() => ({}));
|
|
226
|
-
// Never in pier's own directory.
|
|
227
|
-
if (typeof body.cwd !== "string" || !body.cwd)
|
|
228
|
-
return c.json({ error: "cwd required" }, 400);
|
|
229
|
-
// Resolved like the seam does (agent/pi.ts), or the nascent row is the one
|
|
230
|
-
// place a symlinked directory still looks like a project of its own.
|
|
231
|
-
const cwd = await realpath(body.cwd).catch(() => body.cwd);
|
|
232
|
-
const session = await factory.create({ cwd });
|
|
233
|
-
const createdAt = Date.now();
|
|
234
|
-
nascent.set(session.id, { cwd, createdAt });
|
|
235
|
-
router.attach({ channelId: "web", conversationId: session.id }, session);
|
|
236
|
-
// Created is as good as spoken to: a row born below the working set would
|
|
237
|
-
// jump on the first message.
|
|
238
|
-
state.promote(session.id);
|
|
239
|
-
hub.emitWorkspace({ type: "sessions-changed" });
|
|
240
|
-
return c.json({ id: session.id }, 201);
|
|
241
|
-
});
|
|
242
200
|
app.post("/api/sessions/:id/read", (c) => {
|
|
243
201
|
const id = c.req.param("id");
|
|
244
202
|
if (state.unread(id)) {
|
|
@@ -253,7 +211,10 @@ export function createServer({ factory, router, hub, sessions: state, config, pa
|
|
|
253
211
|
app.use("/api/sessions/:id/turns/:index/steps", compress());
|
|
254
212
|
guarded(app, "GET", "/api/sessions/:id/history", 404, async (c) => {
|
|
255
213
|
const id = c.req.param("id");
|
|
256
|
-
|
|
214
|
+
// Nothing live to snapshot: no cursor, no state, the transcript and its run cards.
|
|
215
|
+
if (older(id))
|
|
216
|
+
return c.json({ turns: (await turnsOf(id)).map(slim), backgroundRuns: backgroundRuns?.(id) ?? [], skills: [], readonly: true });
|
|
217
|
+
const session = await ensure(id);
|
|
257
218
|
// Async seam reads can straddle an event. Never label older content with a
|
|
258
219
|
// newer cursor, and never spin indefinitely if the session stays busy.
|
|
259
220
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
@@ -268,10 +229,9 @@ export function createServer({ factory, router, hub, sessions: state, config, pa
|
|
|
268
229
|
state: session.state,
|
|
269
230
|
context: session.contextUsage ?? null,
|
|
270
231
|
thinkingLevel: session.thinkingLevel,
|
|
271
|
-
queue,
|
|
272
|
-
queueRecovery: router.recoveryOf(id),
|
|
273
|
-
queueUncertain: router.queueUncertain(id),
|
|
232
|
+
queue: { ...queue, parked: parkedMessages?.(id) ?? [] },
|
|
274
233
|
backgroundRuns: backgroundRuns?.(id) ?? [],
|
|
234
|
+
skills: session.skills(),
|
|
275
235
|
});
|
|
276
236
|
}
|
|
277
237
|
log.warn(`snapshot for ${id} changed during all 3 reads`);
|
|
@@ -285,7 +245,7 @@ export function createServer({ factory, router, hub, sessions: state, config, pa
|
|
|
285
245
|
const index = Number(c.req.param("index"));
|
|
286
246
|
if (!Number.isInteger(index) || index < 0)
|
|
287
247
|
return c.json({ error: "index required" }, 400);
|
|
288
|
-
const turn = (await (
|
|
248
|
+
const turn = (await turnsOf(c.req.param("id")))[index];
|
|
289
249
|
if (!turn)
|
|
290
250
|
return c.json({ error: `no turn at index ${index}` }, 404);
|
|
291
251
|
return c.json({ steps: turn.steps ?? [] });
|
|
@@ -371,16 +331,52 @@ export function createServer({ factory, router, hub, sessions: state, config, pa
|
|
|
371
331
|
return c.json({ error: "text required" }, 400);
|
|
372
332
|
}
|
|
373
333
|
const mode = body.mode === "steer" || body.mode === "followUp" ? body.mode : "auto";
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
334
|
+
sentByOperator(id);
|
|
335
|
+
try {
|
|
336
|
+
const { sessionId } = await router.dispatch({
|
|
337
|
+
key: { channelId: "web", conversationId: id },
|
|
338
|
+
senderId: "web",
|
|
339
|
+
// Named: in a session also reached from a group chat, an unheaded message
|
|
340
|
+
// is attributed to whoever spoke last (core/identity.ts).
|
|
341
|
+
sender: { id: "web", name: "operator" },
|
|
342
|
+
text: body.text,
|
|
343
|
+
mode,
|
|
344
|
+
});
|
|
345
|
+
return c.json({ sessionId }, 202);
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
348
|
+
if (err instanceof SkillAmbiguous)
|
|
349
|
+
return c.json({ error: err.message }, 409);
|
|
350
|
+
throw err;
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
/** A rotation re-lists every surface. */
|
|
354
|
+
const reached = (head) => {
|
|
355
|
+
if (head.rotated)
|
|
356
|
+
hub.emitWorkspace({ type: "sessions-changed" });
|
|
357
|
+
return head;
|
|
358
|
+
};
|
|
359
|
+
// The chain, newest first; the client pages back through it with /history.
|
|
360
|
+
app.get("/api/continuous", (c) => c.json({ chain: continuous.members(), rotateAt: CHAIN_FULL_TOKENS }));
|
|
361
|
+
// The drawer's run rows.
|
|
362
|
+
app.get("/api/continuous/open", (c) => c.json(openItems?.() ?? { items: [], unlisted: [], designs: [] }));
|
|
363
|
+
// ⋯ → Status: `/status`'s answer, without posting it.
|
|
364
|
+
app.get("/api/continuous/status", (c) => c.json(continuous.status()));
|
|
365
|
+
// The alias send: the head is resolved (and rotated) here, so a rotation
|
|
366
|
+
// between the client's snapshot and its send cannot land on an old head.
|
|
367
|
+
guarded(app, "POST", "/api/continuous/messages", 400, async (c) => {
|
|
368
|
+
const body = await c.req.json().catch(() => null);
|
|
369
|
+
if (!body || typeof body.text !== "string" || !body.text.trim())
|
|
370
|
+
return c.json({ error: "text required" }, 400);
|
|
371
|
+
const mode = body.mode === "steer" || body.mode === "followUp" ? body.mode : "auto";
|
|
372
|
+
try {
|
|
373
|
+
return c.json(reached(await continuous.send({ senderId: "web", sender: { id: "web", name: "operator" }, text: body.text, mode })), 202);
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
if (err instanceof ChatCommandRefused || err instanceof SkillAmbiguous)
|
|
377
|
+
return c.json({ error: err.message }, 409);
|
|
378
|
+
throw err;
|
|
379
|
+
}
|
|
384
380
|
});
|
|
385
381
|
// Edit a user turn: rewind to just before it — dropping every turn after it —
|
|
386
382
|
// then re-dispatch the edited text.
|
|
@@ -394,6 +390,8 @@ export function createServer({ factory, router, hub, sessions: state, config, pa
|
|
|
394
390
|
// Before touching anything: a refused dispatch must not cost a rewound transcript.
|
|
395
391
|
if (router.isDraining())
|
|
396
392
|
return c.json({ error: "Pier is restarting — try again in a moment" }, 503);
|
|
393
|
+
if (older(id))
|
|
394
|
+
return c.json({ error: "an earlier session of the continuous conversation is read-only" }, 409);
|
|
397
395
|
const session = await ensure(id);
|
|
398
396
|
if (session.state === "streaming")
|
|
399
397
|
return c.json({ error: "busy — stop the turn first" }, 409);
|
|
@@ -405,6 +403,7 @@ export function createServer({ factory, router, hub, sessions: state, config, pa
|
|
|
405
403
|
await session.rewindToUserTurn(index);
|
|
406
404
|
// The rewind took the speaker headers out of the context too.
|
|
407
405
|
router.forgetSender(id);
|
|
406
|
+
sentByOperator(id);
|
|
408
407
|
await router.dispatch({
|
|
409
408
|
key: { channelId: "web", conversationId: id },
|
|
410
409
|
senderId: "web",
|
|
@@ -414,7 +413,7 @@ export function createServer({ factory, router, hub, sessions: state, config, pa
|
|
|
414
413
|
});
|
|
415
414
|
return c.json({ ok: true }, 202);
|
|
416
415
|
});
|
|
417
|
-
// Core owns exclusion and
|
|
416
|
+
// Core owns exclusion and reports a failed delivery with the originals.
|
|
418
417
|
guarded(app, "POST", "/api/sessions/:id/queue/deliver", 404, async (c) => {
|
|
419
418
|
const id = c.req.param("id");
|
|
420
419
|
const body = await c.req.json().catch(() => null);
|
|
@@ -422,6 +421,7 @@ export function createServer({ factory, router, hub, sessions: state, config, pa
|
|
|
422
421
|
if (mode !== "steer" && mode !== "restart") {
|
|
423
422
|
return c.json({ error: "mode must be steer or restart" }, 400);
|
|
424
423
|
}
|
|
424
|
+
sentByOperator(id);
|
|
425
425
|
return queueResponse(async () => ({ submitted: await router.deliverQueue(id, mode) }), 202);
|
|
426
426
|
});
|
|
427
427
|
guarded(app, "POST", "/api/sessions/:id/queue/recall", 404, async (c) => {
|
|
@@ -431,32 +431,6 @@ export function createServer({ factory, router, hub, sessions: state, config, pa
|
|
|
431
431
|
return { messages: [...steering, ...followUp] };
|
|
432
432
|
});
|
|
433
433
|
});
|
|
434
|
-
guarded(app, "POST", "/api/sessions/:id/queue/recovery/:batchId/ack", 404, async (c) => queueResponse(async () => {
|
|
435
|
-
router.acknowledgeRecovery(c.req.param("id"), c.req.param("batchId"));
|
|
436
|
-
return { ok: true };
|
|
437
|
-
}));
|
|
438
|
-
// Refused while streaming: Pi's compaction aborts a running turn, and losing
|
|
439
|
-
// one is not what the button offered. The result arrives on the stream as
|
|
440
|
-
// `context-compacted`.
|
|
441
|
-
guarded(app, "POST", "/api/sessions/:id/compact", 404, async (c) => {
|
|
442
|
-
const session = await ensure(c.req.param("id"));
|
|
443
|
-
if (session.state === "streaming")
|
|
444
|
-
return c.json({ error: "busy — stop the turn first" }, 409);
|
|
445
|
-
// The check above is not the lock: two clicks pass it on the same tick, and
|
|
446
|
-
// the seam's refusal must keep the "not now" status, not read as "no such session".
|
|
447
|
-
return await session.compact().then(() => c.json({ ok: true }, 202), (err) => c.json({ error: String(err) }, 409));
|
|
448
|
-
});
|
|
449
|
-
// Not refused while streaming: a rename has nothing to do with the turn running.
|
|
450
|
-
guarded(app, "POST", "/api/sessions/:id/rename", 404, async (c) => {
|
|
451
|
-
const body = await c.req.json().catch(() => null);
|
|
452
|
-
if (typeof body?.name !== "string")
|
|
453
|
-
return c.json({ error: "name required" }, 400);
|
|
454
|
-
const id = c.req.param("id");
|
|
455
|
-
await (await ensure(id)).rename(body.name.trim().slice(0, SESSION_TITLE_MAX));
|
|
456
|
-
// The transcript is the answer; the event tells surfaces to re-read it.
|
|
457
|
-
hub.emitWorkspace({ type: "sessions-changed" });
|
|
458
|
-
return c.json({ ok: true });
|
|
459
|
-
});
|
|
460
434
|
app.post("/api/sessions/:id/abort", async (c) => {
|
|
461
435
|
const id = c.req.param("id");
|
|
462
436
|
await router.abort(id);
|
|
@@ -535,9 +509,10 @@ export function createServer({ factory, router, hub, sessions: state, config, pa
|
|
|
535
509
|
onUnlocked,
|
|
536
510
|
onSettingsChanged: () => recycle("instance settings"),
|
|
537
511
|
passkeys,
|
|
512
|
+
subscribed,
|
|
538
513
|
});
|
|
539
514
|
registerProviderRoutes(app, providers, () => recycle("provider configuration"));
|
|
540
|
-
registerConfigRoutes(app, { factory, config, onConfigWritten: () => recycle("an agent file") });
|
|
515
|
+
registerConfigRoutes(app, { factory, config, onConfigWritten: () => recycle("an agent file"), subscribed });
|
|
541
516
|
registerPackageRoutes(app, { factory, packages, onConfigWritten: () => recycle("the package registry") });
|
|
542
517
|
registerFsRoutes(app);
|
|
543
518
|
registerExplorerRoutes(app);
|