@timqi/pier 0.0.29 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/agent/config.js +22 -0
  2. package/dist/agent/events.js +4 -1
  3. package/dist/agent/listing.js +114 -16
  4. package/dist/agent/pi.js +158 -16
  5. package/dist/channels/lark-panel.js +1 -1
  6. package/dist/core/inbox.js +1 -1
  7. package/dist/core/router.js +4 -0
  8. package/dist/core/types.js +13 -1
  9. package/dist/db.js +30 -0
  10. package/dist/drain.js +1 -1
  11. package/dist/extensions/web/content.js +2 -2
  12. package/dist/main.js +8 -1
  13. package/dist/secrets.js +1 -1
  14. package/dist/service.js +2 -2
  15. package/dist/settings.js +31 -10
  16. package/dist/tasks/agent.js +109 -73
  17. package/dist/tasks/callbacks.js +1 -3
  18. package/dist/tasks/command.js +27 -8
  19. package/dist/tasks/execution.js +6 -4
  20. package/dist/tasks/groups.js +35 -26
  21. package/dist/tasks/messages.js +33 -27
  22. package/dist/tasks/outbox.js +48 -21
  23. package/dist/tasks/routes.js +1 -4
  24. package/dist/tasks/runs.js +10 -4
  25. package/dist/tasks/service.js +24 -12
  26. package/dist/tasks/store.js +17 -3
  27. package/dist/tasks/tool.js +97 -25
  28. package/dist/tools.js +2 -2
  29. package/dist/update.js +1 -1
  30. package/dist/web/instance.js +10 -3
  31. package/dist/web/providers.js +9 -2
  32. package/dist/web/public/assets/{activity-D3m4L2IL.js → activity-Bl3vZukb.js} +2 -2
  33. package/dist/web/public/assets/activity-Bl3vZukb.js.br +0 -0
  34. package/dist/web/public/assets/activity-Bl3vZukb.js.gz +0 -0
  35. package/dist/web/public/assets/boards-DYuf4Mlj.js +1 -0
  36. package/dist/web/public/assets/boards-DYuf4Mlj.js.br +0 -0
  37. package/dist/web/public/assets/boards-DYuf4Mlj.js.gz +0 -0
  38. package/dist/web/public/assets/explorer-qJH_9nTE.js +4 -0
  39. package/dist/web/public/assets/explorer-qJH_9nTE.js.br +0 -0
  40. package/dist/web/public/assets/explorer-qJH_9nTE.js.gz +0 -0
  41. package/dist/web/public/assets/index-Dqdb-Eqt.js +85 -0
  42. package/dist/web/public/assets/index-Dqdb-Eqt.js.br +0 -0
  43. package/dist/web/public/assets/index-Dqdb-Eqt.js.gz +0 -0
  44. package/dist/web/public/assets/index-DzmMzvi_.css +2 -0
  45. package/dist/web/public/assets/index-DzmMzvi_.css.br +0 -0
  46. package/dist/web/public/assets/index-DzmMzvi_.css.gz +0 -0
  47. package/dist/web/public/assets/runs-BLJu7EXN.js +1 -0
  48. package/dist/web/public/assets/runs-BLJu7EXN.js.br +0 -0
  49. package/dist/web/public/assets/runs-BLJu7EXN.js.gz +0 -0
  50. package/dist/web/public/assets/settings-BrdVh-Zi.js +5 -0
  51. package/dist/web/public/assets/settings-BrdVh-Zi.js.br +0 -0
  52. package/dist/web/public/assets/settings-BrdVh-Zi.js.gz +0 -0
  53. package/dist/web/public/assets/task-runs-CeQS1rxa.js +3 -0
  54. package/dist/web/public/assets/task-runs-CeQS1rxa.js.br +0 -0
  55. package/dist/web/public/assets/task-runs-CeQS1rxa.js.gz +0 -0
  56. package/dist/web/public/assets/tasks-bcb3fYdK.js +4 -0
  57. package/dist/web/public/assets/tasks-bcb3fYdK.js.br +0 -0
  58. package/dist/web/public/assets/tasks-bcb3fYdK.js.gz +0 -0
  59. package/dist/web/public/index.html +76 -120
  60. package/dist/web/public/index.html.br +0 -0
  61. package/dist/web/public/index.html.gz +0 -0
  62. package/dist/web/public/manifest.webmanifest +2 -2
  63. package/dist/web/public/manifest.webmanifest.br +0 -0
  64. package/dist/web/public/manifest.webmanifest.gz +0 -0
  65. package/dist/web/server.js +40 -8
  66. package/dist/web/session-state.js +5 -5
  67. package/package.json +2 -1
  68. package/skills/pier-tasks/SKILL.md +145 -160
  69. package/dist/web/public/assets/activity-D3m4L2IL.js.br +0 -0
  70. package/dist/web/public/assets/activity-D3m4L2IL.js.gz +0 -0
  71. package/dist/web/public/assets/boards-BIObcQeX.js +0 -1
  72. package/dist/web/public/assets/boards-BIObcQeX.js.br +0 -0
  73. package/dist/web/public/assets/boards-BIObcQeX.js.gz +0 -0
  74. package/dist/web/public/assets/explorer-C_rSWPNB.js +0 -4
  75. package/dist/web/public/assets/explorer-C_rSWPNB.js.br +0 -0
  76. package/dist/web/public/assets/explorer-C_rSWPNB.js.gz +0 -0
  77. package/dist/web/public/assets/index-CX3fYZY5.css +0 -2
  78. package/dist/web/public/assets/index-CX3fYZY5.css.br +0 -0
  79. package/dist/web/public/assets/index-CX3fYZY5.css.gz +0 -0
  80. package/dist/web/public/assets/index-uFsZkKOQ.js +0 -85
  81. package/dist/web/public/assets/index-uFsZkKOQ.js.br +0 -0
  82. package/dist/web/public/assets/index-uFsZkKOQ.js.gz +0 -0
  83. package/dist/web/public/assets/runs-Ch6DZq6O.js +0 -1
  84. package/dist/web/public/assets/runs-Ch6DZq6O.js.br +0 -0
  85. package/dist/web/public/assets/runs-Ch6DZq6O.js.gz +0 -0
  86. package/dist/web/public/assets/settings-BWcEIEcv.js +0 -5
  87. package/dist/web/public/assets/settings-BWcEIEcv.js.br +0 -0
  88. package/dist/web/public/assets/settings-BWcEIEcv.js.gz +0 -0
  89. package/dist/web/public/assets/task-runs-DPkwv2UE.js +0 -3
  90. package/dist/web/public/assets/task-runs-DPkwv2UE.js.br +0 -0
  91. package/dist/web/public/assets/task-runs-DPkwv2UE.js.gz +0 -0
  92. package/dist/web/public/assets/tasks-DTiCi2mH.js +0 -4
  93. package/dist/web/public/assets/tasks-DTiCi2mH.js.br +0 -0
  94. package/dist/web/public/assets/tasks-DTiCi2mH.js.gz +0 -0
@@ -7,17 +7,19 @@
7
7
  workbench zoomed by a stray gesture has no visible way back — said again
8
8
  as `touch-action` (style.css) for the engines that ignore this pair. -->
9
9
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
10
- <meta name="theme-color" content="#fafafa" />
10
+ <meta name="theme-color" content="#f1f4f3" />
11
11
  <!-- Ahead of the stylesheet, because a workbench that flashes white and then
12
12
  goes dark reads as a bug. This is the only place "system" is resolved
13
13
  before the bundle exists; theme.ts owns the choice from there on. -->
14
14
  <script>
15
- try {
16
- var t = localStorage.getItem("pier.theme");
17
- document.documentElement.dataset.theme =
18
- t === "light" || t === "dark" ? t
19
- : matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
20
- } catch (e) { /* storage denied (private mode, blocked cookies): light it is */ }
15
+ var t;
16
+ try { t = localStorage.getItem("pier.theme"); }
17
+ catch (e) { /* storage denied: follow the system for this visit */ }
18
+ var mode = t === "light" || t === "dark" ? t
19
+ : matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
20
+ document.documentElement.dataset.theme = mode;
21
+ // Before CSS loads, mirror its canvas token so installed chrome never flashes.
22
+ document.querySelector('meta[name="theme-color"]').content = mode === "dark" ? "#14171a" : "#f1f4f3";
21
23
  </script>
22
24
  <!-- Installed as a home-screen app: standalone window, own icon. The
23
25
  manifest shares the workbench's cookie boundary. -->
@@ -35,8 +37,8 @@
35
37
  which style.css pays for with the safe-area inset. -->
36
38
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
37
39
  <title>Pier</title>
38
- <script type="module" crossorigin src="/assets/index-uFsZkKOQ.js"></script>
39
- <link rel="stylesheet" crossorigin href="/assets/index-CX3fYZY5.css">
40
+ <script type="module" crossorigin src="/assets/index-Dqdb-Eqt.js"></script>
41
+ <link rel="stylesheet" crossorigin href="/assets/index-DzmMzvi_.css">
40
42
  </head>
41
43
  <!-- The document never scrolls: this is a fixed-viewport workbench, and every
42
44
  scrollable region is an inner pane. h-dvh, not h-screen, because 100vh can
@@ -52,11 +54,9 @@
52
54
  control in the same band. -->
53
55
  <button type="button" id="rail-toggle" aria-label="Toggle sidebar"
54
56
  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">
55
- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" class="h-3.5 w-3.5">
56
- <path d="M9.5 4 5.5 8l4 4" stroke-linecap="round" stroke-linejoin="round" />
57
- </svg>
57
+ <span data-icon="ChevronLeft" class="h-3.5 w-3.5"></span>
58
58
  </button>
59
- <aside id="sidebar" class="flex flex-col overflow-hidden border-r border-neutral-200 bg-neutral-50">
59
+ <aside id="sidebar" class="flex flex-col overflow-hidden border-r border-neutral-200 bg-neutral-50 font-sans">
60
60
  <header class="flex h-12 flex-none items-center gap-2 px-3">
61
61
  <!-- The same mark as the tab and the home-screen icon, so an installed
62
62
  Pier looks like the thing you installed. -->
@@ -76,72 +76,59 @@
76
76
  <button type="button" id="theme-toggle" aria-label="Theme"
77
77
  class="icon-btn text-[13px]"></button>
78
78
  </header>
79
+ <div class="flex flex-none items-center gap-1.5 px-3 pb-1">
80
+ <button type="button" id="new-session" aria-label="New session"
81
+ class="btn btn-primary flex min-w-0 flex-1 items-center justify-center gap-2 whitespace-nowrap px-3">
82
+ <span data-icon="Plus" aria-hidden="true" class="h-3.5 w-3.5"></span> New session
83
+ </button>
84
+ <button type="button" id="open-palette" aria-label="Search sessions, messages and Console"
85
+ 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">
86
+ <span data-icon="Search" class="h-5 w-5 flex-none"></span>
87
+ <span class="sr-only">Search</span>
88
+ </button>
89
+ </div>
79
90
  <details id="console-section" open class="flex-none border-b border-neutral-200/70">
80
- <summary class="flex cursor-pointer select-none items-center gap-1 px-3 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-wide text-neutral-500 hover:bg-neutral-100">
81
- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true" class="chev h-3 w-3"><path d="m6 4 4 4-4 4" stroke-linecap="round" stroke-linejoin="round" /></svg>Console
91
+ <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">
92
+ <span data-icon="ChevronRight" class="chev h-3 w-3"></span>Console
82
93
  </summary>
83
- <ul class="pb-2 text-[13.5px]">
94
+ <ul class="pb-1">
84
95
  <!-- One row for the tasks area: Tasks, Runs and Activity are tabs of
85
96
  the Automation page (views.ts), not three sidebar entries. -->
86
97
  <li><button type="button" id="open-tasks"
87
- class="flex w-full cursor-pointer items-center gap-2 py-1.5 pl-6 pr-3 text-left hover:bg-neutral-100">
88
- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" class="h-3.5 w-3.5 flex-none text-neutral-400">
89
- <path d="M6 3h8M6 8h8M6 13h8M2 3h1M2 8h1M2 13h1" />
90
- </svg>Automation</button></li>
98
+ class="flex w-full cursor-pointer items-center gap-2 pr-3 text-left hover:bg-neutral-100">
99
+ <span data-icon="ListTodo" class="h-3.5 w-3.5 flex-none text-neutral-400"></span>Automation</button></li>
91
100
  <li><button type="button" id="open-boards"
92
- class="flex w-full cursor-pointer items-center gap-2 py-1.5 pl-6 pr-3 text-left hover:bg-neutral-100">
93
- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" class="h-3.5 w-3.5 flex-none text-neutral-400">
94
- <rect x="2" y="2" width="12" height="12" rx="1.5" /><path d="M2 6.5h12M6.5 6.5V14" />
95
- </svg>Boards</button></li>
101
+ class="flex w-full cursor-pointer items-center gap-2 pr-3 text-left hover:bg-neutral-100">
102
+ <span data-icon="LayoutDashboard" class="h-3.5 w-3.5 flex-none text-neutral-400"></span>Boards</button></li>
96
103
  <li><button type="button" id="open-settings"
97
- class="flex w-full cursor-pointer items-center gap-2 py-1.5 pl-6 pr-3 text-left hover:bg-neutral-100">
98
- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" class="h-3.5 w-3.5 flex-none text-neutral-400">
99
- <!-- Teeth every 60° about (8,8); the hub is that same centre. -->
100
- <path d="M6.8 2.2h2.4l.32 1.6a4.9 4.9 0 0 1 1.36.79l1.55-.53 1.2 2.08-1.23 1.07a4.9 4.9 0 0 1 0 1.58l1.23 1.07-1.2 2.08-1.55-.53a4.9 4.9 0 0 1-1.36.79l-.32 1.6H6.8l-.32-1.6a4.9 4.9 0 0 1-1.36-.79l-1.55.53-1.2-2.08 1.23-1.07a4.9 4.9 0 0 1 0-1.58L2.37 6.14l1.2-2.08 1.55.53a4.9 4.9 0 0 1 1.36-.79z" stroke-linejoin="round" />
101
- <circle cx="8" cy="8" r="1.9" />
102
- </svg>Settings</button></li>
104
+ class="flex w-full cursor-pointer items-center gap-2 pr-3 text-left hover:bg-neutral-100">
105
+ <span data-icon="Settings" class="h-3.5 w-3.5 flex-none text-neutral-400"></span>Settings</button></li>
103
106
  </ul>
104
107
  </details>
105
- <div class="flex items-center gap-1 px-3 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-wide text-neutral-500">
106
- Sessions
107
- <button type="button" id="open-archive"
108
- class="icon-btn ml-auto" aria-label="Search sessions and Console">
109
- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" class="h-3.5 w-3.5">
110
- <circle cx="7" cy="7" r="4.25" /><path d="M10.2 10.2 14 14" stroke-linecap="round" />
111
- </svg>
112
- </button>
113
- <button type="button" id="new-session" title="New session"
114
- class="icon-btn" aria-label="New session">
115
- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" class="h-3.5 w-3.5">
116
- <rect x="2.25" y="2.25" width="11.5" height="11.5" rx="2.5" />
117
- <path d="M8 5.5v5M5.5 8h5" stroke-linecap="round" />
118
- </svg>
119
- </button>
120
- </div>
121
- <div id="session-list" class="flex-1 overflow-y-auto text-[13.5px]"></div>
108
+ <div class="flex-none px-3 pb-1 pt-2 text-xs font-semibold leading-5 text-neutral-500">Sessions</div>
109
+ <div id="session-list" class="min-h-0 flex-1 overflow-y-auto"></div>
122
110
  </aside>
123
111
 
124
- <main class="flex min-w-0 flex-col overflow-hidden">
112
+ <main class="min-w-0 overflow-hidden">
125
113
  <!-- Mobile top bar: replaces the chat header below md (which hides), so a
126
114
  Console view keeps the same way back to the drawer as a chat does. -->
127
- <header id="mobile-bar" class="flex h-12 flex-none items-center gap-1 border-b border-neutral-200 px-1.5 md:hidden">
115
+ <header id="mobile-bar" class="flex flex-none items-center gap-1 px-1.5 md:hidden">
128
116
  <button type="button" id="drawer-toggle" aria-label="Open sidebar"
129
- class="relative flex h-10 w-10 flex-none cursor-pointer items-center justify-center rounded-md text-neutral-500 active:bg-neutral-200">
130
- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" class="h-5 w-5">
131
- <path d="M2.5 4h11M2.5 8h11M2.5 12h11" />
132
- </svg>
117
+ class="icon-btn relative h-11 w-11 text-neutral-500">
118
+ <span data-icon="Menu" class="h-4.5 w-4.5"></span>
133
119
  </button>
134
120
  <span id="mobile-title" class="min-w-0 flex-1 truncate text-[16px] font-medium"></span>
121
+ <!-- Both bar controls are the shared round icon button at a thumb's size
122
+ (44px), with the chat header's own 14px ⋯ glyph — not a row of
123
+ oversized dots. -->
135
124
  <button type="button" id="mobile-menu" aria-label="Session actions"
136
- class="hidden h-10 w-10 flex-none cursor-pointer items-center justify-center rounded-md text-[20px] leading-none text-neutral-500 active:bg-neutral-200">⋯</button>
125
+ class="icon-btn hidden h-11 w-11"><span data-icon="Ellipsis" class="h-3.5 w-3.5"></span></button>
137
126
  </header>
138
- <!-- Automation hosts three views under one title and tab strip; the
139
- strip's active pill tracks the route (#/tasks, #/runs, #/activity). -->
127
+ <!-- Automation hosts three views under one head — title, tabs and the
128
+ page's primary action on one strip, drawn by views.ts; its active pill
129
+ tracks the route (#/tasks, #/runs, #/activity). -->
140
130
  <section id="automation-view" class="hidden min-h-0 flex-1 flex-col">
141
- <header class="flex h-10 flex-none items-center gap-3 border-b border-neutral-200 bg-white px-4 max-md:hidden">
142
- <span class="font-medium">Automation</span>
143
- </header>
144
- <div id="automation-tabs" class="tabstrip bg-white"></div>
131
+ <header id="automation-tabs" class="pagehead"></header>
145
132
  <section id="tasks-view" class="hidden min-h-0 flex-1 flex-col"></section>
146
133
  <section id="runs-view" class="hidden min-h-0 flex-1 flex-col"></section>
147
134
  <section id="activity-view" class="hidden min-h-0 flex-1 flex-col"></section>
@@ -157,7 +144,7 @@
157
144
  <!-- Inline session meta: model · reasoning · current context size. -->
158
145
  <div id="session-meta" class="hidden flex-none items-center gap-1 text-[11px]"></div>
159
146
  <button type="button" id="chat-menu" title="Session actions" aria-label="Session actions"
160
- class="icon-btn">⋯</button>
147
+ class="icon-btn"><span data-icon="Ellipsis" class="h-3.5 w-3.5"></span></button>
161
148
  </div>
162
149
  </header>
163
150
  <!-- The transcript's own type scale, inherited by rows, chips and markdown. -->
@@ -166,36 +153,21 @@
166
153
  <div id="turns" data-gallery class="flex-1 overflow-y-auto pb-5 pt-2 text-[15px] leading-[1.55]"></div>
167
154
  <!-- Pi's queue primitives are whole-queue (steer / clear), so the actions
168
155
  live on the header row and read as "the queue", never per message. -->
169
- <div id="queue-panel" class="hidden flex-col border-t border-neutral-200 px-3 py-1.5">
156
+ <div id="queue-panel" class="hidden flex-col border-t border-neutral-200 px-4 pb-3 pt-2">
170
157
  <div class="flex h-7 items-center gap-2">
171
158
  <span id="queue-label" class="text-[11px] font-semibold uppercase tracking-wide text-amber-700">Queued</span>
172
159
  <div class="ml-auto flex items-center gap-0.5 text-amber-700">
173
160
  <button type="button" id="queue-steer" title="Send the whole queue now into the running turn" aria-label="Send queue now"
174
161
  class="flex h-7 w-7 cursor-pointer items-center justify-center rounded-md hover:bg-amber-100 active:bg-amber-200">
175
- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"
176
- stroke-linecap="round" stroke-linejoin="round" class="h-3.5 w-3.5">
177
- <path d="M14.5 1.5 7 9m7.5-7.5-4.2 12.6L7 9 1.7 6.7l12.8-5.2z" />
178
- </svg>
162
+ <span data-icon="Send" class="h-3.5 w-3.5"></span>
179
163
  </button>
180
164
  <button type="button" id="queue-restart" title="Stop the running turn and send the whole queue as a new prompt" aria-label="Stop and send queue"
181
165
  class="flex h-7 w-7 cursor-pointer items-center justify-center rounded-md hover:bg-amber-100 active:bg-amber-200">
182
- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"
183
- stroke-linecap="round" stroke-linejoin="round" class="h-3.5 w-3.5">
184
- <!-- The arc's ink sits high-right of the box; centre it so the
185
- three icons share one baseline. -->
186
- <g transform="translate(-0.8 1.9)">
187
- <path d="M13 5.2V1.8m0 0H9.6M13 1.8A6 6 0 1 0 14 9" />
188
- <path d="M6.2 8h4M8.5 5.7 10.8 8l-2.3 2.3" />
189
- </g>
190
- </svg>
166
+ <span data-icon="RotateCw" class="h-3.5 w-3.5"></span>
191
167
  </button>
192
168
  <button type="button" id="queue-recall" title="Return queued messages to the composer" aria-label="Recall queued messages"
193
169
  class="flex h-7 w-7 cursor-pointer items-center justify-center rounded-md hover:bg-amber-100 active:bg-amber-200">
194
- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"
195
- stroke-linecap="round" stroke-linejoin="round" class="h-3.5 w-3.5">
196
- <!-- Ink centred on the 16×16 box, or the glyph sits low next to its neighbours. -->
197
- <path d="M5 3 2 6l3 3M2.5 6H10a3.5 3.5 0 0 1 0 7H8" />
198
- </svg>
170
+ <span data-icon="Undo2" class="h-3.5 w-3.5"></span>
199
171
  </button>
200
172
  </div>
201
173
  </div>
@@ -203,66 +175,50 @@
203
175
  (18px leading) rather than shrinking the turns pane any further. -->
204
176
  <ul id="queue-rows" class="flex max-h-[6rem] flex-col gap-1 overflow-y-auto overscroll-contain"></ul>
205
177
  </div>
206
- <form id="composer" class="flex-none px-4 pb-3 pt-2">
178
+ <form id="composer" class="flex-none px-4 pb-4 pt-2">
207
179
  <div class="glass rounded-[1.75rem] border border-neutral-200 px-3 py-2.5 transition-shadow focus-within:border-indigo-400">
208
180
  <div id="image-strip" data-gallery class="hidden flex-wrap gap-2 px-1 pb-1.5 pt-1"></div>
209
181
  <!-- items-end keeps the icons on the bottom row while the textarea grows upward -->
210
182
  <div class="flex items-end gap-1.5">
211
183
  <button type="button" id="attach" title="Attach file (or paste / drop)"
212
- class="flex h-7 w-7 flex-none cursor-pointer items-center justify-center rounded-lg text-[17px] leading-none text-neutral-400 hover:bg-neutral-100 hover:text-neutral-600">+</button>
184
+ class="flex h-7 w-7 flex-none cursor-pointer items-center justify-center rounded-lg text-[17px] leading-none text-neutral-400 hover:bg-neutral-100 hover:text-neutral-600" aria-label="Attach file"><span data-icon="Plus" class="h-3.5 w-3.5"></span></button>
213
185
  <input id="attach-input" type="file" multiple class="hidden" />
214
186
  <textarea id="input" rows="1"
215
187
  class="block max-h-48 min-w-0 flex-1 resize-none bg-transparent px-1 py-0.5 focus:outline-none"
216
188
  placeholder="Message…"></textarea>
217
- <button type="button" id="stop" title="Stop the running turn"
189
+ <button type="button" id="stop" title="Stop the running turn" aria-label="Stop the running turn"
218
190
  class="hidden h-7 w-7 flex-none cursor-pointer items-center justify-center rounded-lg bg-red-50 text-red-600 hover:bg-red-100">
219
- <svg viewBox="0 0 16 16" fill="currentColor" class="h-3.5 w-3.5">
220
- <rect x="3.5" y="3.5" width="9" height="9" rx="1.5" />
221
- </svg>
191
+ <span data-icon="Square" class="h-3.5 w-3.5"></span>
222
192
  </button>
223
193
  <!-- Style + icon double as the state display: indigo plane = send, amber clock = queue -->
224
194
  <button type="submit" id="send" title="Send">
225
- <svg id="send-plane" viewBox="0 0 16 16" fill="none" stroke="currentColor"
226
- stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="h-3.5 w-3.5">
227
- <path d="M14.5 1.5 7 9m7.5-7.5-4.2 12.6L7 9 1.7 6.7l12.8-5.2z" />
228
- </svg>
229
- <svg id="send-queue" viewBox="0 0 16 16" fill="none" stroke="currentColor"
230
- stroke-width="1.5" stroke-linecap="round" class="hidden h-3.5 w-3.5">
231
- <circle cx="8" cy="8" r="6" />
232
- <path d="M8 4.75V8l2.4 1.5" />
233
- </svg>
195
+ <span id="send-plane" data-icon="Send" class="h-3.5 w-3.5"></span>
196
+ <span id="send-queue" data-icon="Clock" class="hidden h-3.5 w-3.5"></span>
234
197
  </button>
235
198
  </div>
236
199
  </div>
237
200
  </form>
238
201
  </main>
239
202
 
240
- <dialog id="new-dialog" class="m-auto w-[min(24rem,92vw)] rounded-3xl border border-neutral-200 p-5 shadow-xl backdrop:bg-black/20">
241
- <form id="new-form" method="dialog" class="flex flex-col gap-3">
242
- <h3 class="font-semibold">New session</h3>
243
- <label class="flex flex-col gap-1 text-[13px] text-neutral-600">
244
- Working directory
245
- <input id="new-cwd" type="text" required placeholder="/path/to/project"
246
- class="w-full rounded-md border border-neutral-300 px-2 py-1.5 font-mono text-[13px] focus:border-indigo-400 focus:outline-none" />
247
- </label>
248
- <div class="flex justify-end gap-2">
249
- <button type="button" id="new-cancel" class="btn text-[13px]">Cancel</button>
250
- <button type="submit" class="btn btn-primary text-[13px]">Create</button>
251
- </div>
252
- </form>
253
- </dialog>
254
-
255
203
  <!-- Not m-auto: a palette that grows downward as you type should not also
256
204
  drift upward to stay centred. Pinned near the top instead, which is
257
- where the eye already is and where every other ⌘K lives. -->
258
- <dialog id="archive-dialog" class="mx-auto mb-auto mt-[12vh] w-[35rem] max-w-[90vw] rounded-3xl border border-neutral-200 p-0 shadow-xl backdrop:bg-black/20">
259
- <div class="flex items-center gap-2 border-b border-neutral-200 px-3 py-2">
260
- <input id="archive-search" type="text" placeholder="Search sessions and Console…"
261
- class="w-full bg-transparent text-[14px] focus:outline-none" />
262
- <span id="archive-count" class="flex-none rounded bg-neutral-100 px-1.5 text-[11px] text-neutral-500"></span>
263
- <button type="button" id="archive-close" class="btn flex-none text-[12px]">Close</button>
205
+ where the eye already is and where every other ⌘K lives. A solid
206
+ surface (docs/design/06-ui-ux.md): glass is for chrome, and this is
207
+ something you read. The edge and shadow tokens are set in style.css;
208
+ a click on the backdrop lands on the dialog element itself and closes
209
+ it (palette.ts). -->
210
+ <dialog id="palette" class="mx-auto mb-auto mt-[12vh] w-[36rem] max-w-[92vw] rounded-3xl border p-0 backdrop:bg-black/25">
211
+ <div class="flex h-[3.25rem] items-center gap-3 border-b border-neutral-200/70 px-4">
212
+ <span data-icon="Search" aria-hidden="true" class="h-[1.125rem] w-[1.125rem] flex-none text-neutral-400"></span>
213
+ <input id="palette-input" type="text" placeholder="Search sessions, messages and Console…" autocomplete="off" spellcheck="false"
214
+ class="min-w-0 flex-1 bg-transparent text-[1.0625rem] leading-6 placeholder:text-neutral-400 focus:outline-none" />
215
+ <kbd class="flex-none rounded-md border border-neutral-200 px-1.5 font-sans text-[0.6875rem] leading-5 text-neutral-500">esc</kbd>
216
+ </div>
217
+ <ul id="palette-list" role="listbox" aria-label="Results" class="max-h-[60vh] overflow-y-auto px-2 pb-2 text-[0.9375rem]"></ul>
218
+ <div class="flex items-center justify-between border-t border-neutral-200/70 px-4 py-1.5 text-[0.75rem] leading-4 text-neutral-500">
219
+ <span id="palette-keys">↑↓ navigate · ↵ open</span>
220
+ <span id="palette-count"></span>
264
221
  </div>
265
- <ul id="archive-list" class="max-h-[60vh] overflow-y-auto py-1 text-[13.5px]"></ul>
266
222
  </dialog>
267
223
 
268
224
  <!-- Full-size view of a thumbnail; a single click (or Esc) closes, a double
@@ -283,8 +239,8 @@
283
239
  </div>
284
240
  <!-- Siblings of the stage, not children of it: the stage is what scrolls
285
241
  when zoomed, and an arrow inside it would scroll away with the image. -->
286
- <button type="button" id="image-prev" aria-label="Previous image" class="lightbox-nav left-2">‹</button>
287
- <button type="button" id="image-next" aria-label="Next image" class="lightbox-nav right-2">›</button>
242
+ <button type="button" id="image-prev" aria-label="Previous image" class="lightbox-nav left-2"><span data-icon="ChevronLeft" class="h-6 w-6"></span></button>
243
+ <button type="button" id="image-next" aria-label="Next image" class="lightbox-nav right-2"><span data-icon="ChevronRight" class="h-6 w-6"></span></button>
288
244
  </dialog>
289
245
 
290
246
  <!-- Text preview of an agent attachment — the Files view's code pane fills
Binary file
Binary file
@@ -8,8 +8,8 @@
8
8
  "start_url": "/",
9
9
  "scope": "/",
10
10
  "display": "standalone",
11
- "background_color": "#fafafa",
12
- "theme_color": "#fafafa",
11
+ "background_color": "#f1f4f3",
12
+ "theme_color": "#f1f4f3",
13
13
  "categories": ["developer", "productivity", "utilities"],
14
14
  "launch_handler": { "client_mode": "focus-existing" },
15
15
  "icons": [
@@ -76,6 +76,15 @@ export function createServer({ factory, router, hub, sessions: state, config, pr
76
76
  // every client shows the same attention state. Streaming → idle is the
77
77
  // trigger — same transition the client notification uses — and it needs a
78
78
  // start we witnessed, so a session that boots idle stays untouched.
79
+ //
80
+ // Only for the workbench's own sessions: an IM turn was already delivered to
81
+ // the chat it came from and a subagent's to its supervisor by callback, so no
82
+ // look here is owed for either — and none could clear the mark either, since
83
+ // the ack needs the session on screen. Marked anyway, they were a flag that
84
+ // only ever accumulated, and every reader had to subtract them again. The two
85
+ // facts are the ones the list already draws a row from (`present` below): no
86
+ // durable conversation row, and not a session a run made for itself.
87
+ const workbenchOwn = (id) => (channelOf?.(id) ?? "web") === "web" && !(taskSessions?.().has(id) ?? false);
79
88
  const runningNow = new Set();
80
89
  hub.subscribeWorkspace((e) => {
81
90
  if (e.type !== "session-state")
@@ -86,6 +95,8 @@ export function createServer({ factory, router, hub, sessions: state, config, pr
86
95
  }
87
96
  if (!runningNow.delete(e.sessionId))
88
97
  return;
98
+ if (!workbenchOwn(e.sessionId))
99
+ return;
89
100
  state.setUnread(e.sessionId, true);
90
101
  hub.emitWorkspace({ type: "sessions-changed" });
91
102
  });
@@ -135,10 +146,14 @@ export function createServer({ factory, router, hub, sessions: state, config, pr
135
146
  const sessions = await listSessions();
136
147
  for (const s of sessions)
137
148
  nascent.delete(s.id);
138
- // A session created but never prompted would otherwise be listed forever.
139
- for (const [id, n] of nascent)
140
- if (Date.now() - n.createdAt > 86_400_000)
149
+ // A session created but never prompted would otherwise be listed forever —
150
+ // and, since creation ranks it, hold a working-set slot forever too.
151
+ for (const [id, n] of nascent) {
152
+ if (Date.now() - n.createdAt > 86_400_000) {
141
153
  nascent.delete(id);
154
+ state.forget(id);
155
+ }
156
+ }
142
157
  const owned = taskSessions?.() ?? new Set();
143
158
  return [
144
159
  ...[...nascent].map(([id, n]) => ({ id, ...n })),
@@ -158,9 +173,9 @@ export function createServer({ factory, router, hub, sessions: state, config, pr
158
173
  activeRuns: active.get(s.id) ?? 0,
159
174
  });
160
175
  // The rail's top rows are maintained here and nowhere else: a session a
161
- // human speaks to and that is not in the working set already takes the front
162
- // slot (web/session-state.ts). No route — there is no gesture to make, and
163
- // an IM message has no browser to make it from.
176
+ // human speaks to — or creates, below — and that is not in the working set
177
+ // already takes the front slot (web/session-state.ts). No route — there is no
178
+ // gesture to make, and an IM message has no browser to make it from.
164
179
  router.onSpokenTo((id) => {
165
180
  if (state.promote(id))
166
181
  hub.emitWorkspace({ type: "sessions-changed" });
@@ -170,15 +185,32 @@ export function createServer({ factory, router, hub, sessions: state, config, pr
170
185
  const active = activeRuns();
171
186
  return c.json((await allSessions()).map((s) => present(s, flags.get(s.id), active)));
172
187
  });
188
+ // What was said, across every session: the palette's Messages section. The
189
+ // factory owns the index and the ranking; an empty query is an empty answer,
190
+ // not a listing of everything.
191
+ app.get("/api/search", async (c) => {
192
+ const q = (c.req.query("q") ?? "").trim();
193
+ return c.json({ hits: q ? await factory.search(q) : [] });
194
+ });
173
195
  app.post("/api/sessions", async (c) => {
174
196
  const body = await c.req.json().catch(() => ({}));
175
197
  // A session always starts in its project directory — never in pier's own.
176
198
  if (typeof body.cwd !== "string" || !body.cwd)
177
199
  return c.json({ error: "cwd required" }, 400);
178
- const session = await factory.create({ cwd: body.cwd });
200
+ // The seam records the real path (agent/pi.ts), and the row below stands in
201
+ // for a listing until Pi persists the session — up to a day for one never
202
+ // prompted. Resolved here too, or that row is the one place a directory
203
+ // reached through a symlink still looks like a project of its own.
204
+ const cwd = await realpath(body.cwd).catch(() => body.cwd);
205
+ const session = await factory.create({ cwd });
179
206
  const createdAt = Date.now();
180
- nascent.set(session.id, { cwd: body.cwd, createdAt });
207
+ nascent.set(session.id, { cwd, createdAt });
181
208
  router.attach({ channelId: "web", conversationId: session.id }, session);
209
+ // Created is as good as spoken to: the person who clicked New is about to
210
+ // type into it, and a row born below the working set would jump on the
211
+ // first message. Ghosts give the slot back (`ensureLoadable`, the expiry
212
+ // above).
213
+ state.promote(session.id);
182
214
  hub.emitWorkspace({ type: "sessions-changed" });
183
215
  return c.json({ id: session.id }, 201);
184
216
  });
@@ -33,11 +33,11 @@
33
33
  // loses the entire set when the process dies mid-write — a truncated file reads
34
34
  // back as "nothing on top", which is indistinguishable from a fresh install.
35
35
  import { pierDb, transact } from "../db.js";
36
- /** How many rows the rail keeps on top. Larger than the handful anybody
37
- * switches between (3–6), and small enough that every member is somewhere a
38
- * hand can see: a set as deep as the page (20) would count a row nobody has
39
- * scrolled to as "already up there" and never bring it back. */
40
- export const WORKING_SET = 8;
36
+ /** How many rows the rail keeps on top. The handful anybody actually switches
37
+ * between, and small enough that every member is somewhere a hand can see: a
38
+ * set as deep as the page (20) would count a row nobody has scrolled to as
39
+ * "already up there" and never bring it back. */
40
+ export const WORKING_SET = 5;
41
41
  export class SessionStateStore {
42
42
  #db;
43
43
  constructor(db = pierDb()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timqi/pier",
3
- "version": "0.0.29",
3
+ "version": "0.1.0",
4
4
  "description": "A self-hosted workspace for coding agents: web workbench and IM channels in front of Pi sessions",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": "github:timqi/pier",
@@ -50,6 +50,7 @@
50
50
  "dompurify": "^3.4.14",
51
51
  "highlight.js": "^11.12.0",
52
52
  "hono": "^4.13.3",
53
+ "lucide": "1.43.0",
53
54
  "marked": "^18.0.10",
54
55
  "typebox": "^1.3.7"
55
56
  }