create-pmos 0.159.0 → 0.161.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/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-sessions-panel-test.js +24 -0
- package/workspace/guide/ask-the-agent.md +1 -1
- package/workspace/guide/briefing.md +2 -2
- package/workspace/guide/calendar-popup.md +1 -1
- package/workspace/guide/console-calendar.md +2 -2
- package/workspace/guide/console3-sessions.md +21 -3
- package/workspace/guide/index.md +1 -1
- package/workspace/guide/initiative-boards.md +3 -3
- package/workspace/guide/layout.md +5 -3
- package/workspace/guide/notes.md +1 -1
- package/workspace/guide/settings.md +2 -2
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/c3.html +11 -2
- package/workspace/public/pm/c3-dock.js +15 -8
- package/workspace/public/pm/c3-new-session.js +94 -0
- package/workspace/public/pm/c3-plan.js +1 -1
- package/workspace/public/pm/c3-settings.js +2 -2
- package/workspace/public/pm/c3-shell.js +5 -3
- package/workspace/public/pm/c3-tickets.js +1 -1
- package/workspace/public/pm/console3.css +11 -0
- package/workspace/public/pm/pm-work-view.js +1 -1
- package/workspace/public/pm/sessions-panel.js +7 -2
- package/workspace/public/pm/shell-recording.js +1 -1
- package/workspace/public/pm/welcome.js +2 -2
- package/workspace/server.js +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.161.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T07:44:14Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T07:44:14Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T07:44:14Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -121,6 +121,30 @@ function startShell(name) {
|
|
|
121
121
|
ok('D3 End ends the session', !!(await until(gone, 8000)));
|
|
122
122
|
ok('D4 its row leaves Started by me', await waitFor(page, (s) => !document.querySelector(s), sel));
|
|
123
123
|
|
|
124
|
+
/* ── F: New session starts on console3 and opens its terminal here (c3-new-session; it went to the classic app before) ── */
|
|
125
|
+
const before = new Set(((await get('/api/sessions')).sessions || []).map((s) => s.id));
|
|
126
|
+
if (!(await page.isVisible('[data-c3t="new"]').catch(() => false))) await page.click('[data-c3pg="agent-button"]');
|
|
127
|
+
ok('F1 Started by me has a New button', await waitFor(page, () => !!document.querySelector('[data-c3t="new"]')));
|
|
128
|
+
await page.click('[data-c3t="new"]');
|
|
129
|
+
ok('F2 New opens the new-session sheet on this page', await waitFor(page, () => !!document.querySelector('[data-c3ns="form"]')) && page.url() === url0, page.url());
|
|
130
|
+
await page.fill('[data-c3ns="name"]', 'c3 new shell');
|
|
131
|
+
await page.selectOption('[data-c3ns="agent"]', 'shell');
|
|
132
|
+
await page.click('[data-c3ns="start"]');
|
|
133
|
+
const made = await until(async () => ((await get('/api/sessions')).sessions || []).find((s) => !before.has(s.id) && s.name === 'c3 new shell'), 10000);
|
|
134
|
+
ok('F3 Start starts the session on the server', !!made);
|
|
135
|
+
ok('F4 its terminal opens in a sheet on console3', !!made && await waitFor(page, (i) => !!document.querySelector(`[data-c3-session-term="${i}"] .xterm`), made.id) && page.url() === url0, page.url());
|
|
136
|
+
await page.click('[data-shell="sheet"] [aria-label="Close"]');
|
|
137
|
+
await waitFor(page, () => !document.querySelector('[data-c3-session-term]'));
|
|
138
|
+
await page.evaluate(() => window.dispatchEvent(new CustomEvent('pmos:open-session', { detail: {} })));
|
|
139
|
+
ok('F5 an open-session request with no session opens the new-session sheet, not the classic app', await waitFor(page, () => !!document.querySelector('[data-c3ns="form"]')) && page.url() === url0, page.url());
|
|
140
|
+
await page.click('[data-shell="sheet"] [aria-label="Close"]');
|
|
141
|
+
await waitFor(page, () => !document.querySelector('[data-c3ns="form"]'));
|
|
142
|
+
if (await page.isVisible('[data-c3pg="dock-new"]').catch(() => false)) {
|
|
143
|
+
await page.click('[data-c3pg="dock-new"]');
|
|
144
|
+
ok('F6 the dock\'s + opens the new-session sheet on this page', await waitFor(page, () => !!document.querySelector('[data-c3ns="form"]')) && page.url() === url0, page.url());
|
|
145
|
+
await page.click('[data-shell="sheet"] [aria-label="Close"]');
|
|
146
|
+
}
|
|
147
|
+
|
|
124
148
|
ok('E1 the page threw no error', errors.length === 0, errors.join(' | '));
|
|
125
149
|
return finish();
|
|
126
150
|
})().catch((e) => { console.error(e); finish(1); });
|
|
@@ -7,7 +7,7 @@ timestamp: 2026-10-12
|
|
|
7
7
|
|
|
8
8
|
# Ask the agent from the page
|
|
9
9
|
|
|
10
|
-
Five presses in the console (
|
|
10
|
+
Five presses in the console (`/`) start an agent from the page. You do not need a terminal, and you do not copy a request anywhere. Each run is listed in Sessions, so you can follow it there.
|
|
11
11
|
|
|
12
12
|
## Ask the agent on a document
|
|
13
13
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "The Briefing: today's brief"
|
|
4
|
-
description: "The Briefing page of the console (/
|
|
4
|
+
description: "The Briefing page of the console (/): the headline cards, Today, Needs you, Portfolio pulse, and what Done, Not yet and Drop do to an item."
|
|
5
5
|
timestamp: 2026-10-12
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# The Briefing: today's brief
|
|
9
9
|
|
|
10
|
-
The Briefing is the first page of the console at
|
|
10
|
+
The Briefing is the first page of the console at `/`. It has two columns. The left column holds the greeting, the headline cards and Needs you. The right column holds Today and Portfolio pulse. It holds no calendar. The full calendar is the Calendar tab.
|
|
11
11
|
|
|
12
12
|
Review is in the Work page, in its Review lens. Handled, What I could stop and Data you can lean on are in Reports, under Drafts.
|
|
13
13
|
|
|
@@ -7,7 +7,7 @@ timestamp: 2026-10-11
|
|
|
7
7
|
|
|
8
8
|
# Calendar popup
|
|
9
9
|
|
|
10
|
-
The calendar popup shows the Calendar tab's grid in a drawer over any page of the console (
|
|
10
|
+
The calendar popup shows the Calendar tab's grid in a drawer over any page of the console (`/`). You do not leave the page you are on.
|
|
11
11
|
|
|
12
12
|
## Open it
|
|
13
13
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Guide: the Calendar tab of the console page"
|
|
4
|
-
description: "The Calendar tab of the console page (/
|
|
4
|
+
description: "The Calendar tab of the console page (/): Calendar and Notes, the five zooms, the highlights, the layer chips, the markers, the context of a meeting, free time, the tidy-up suggestions and the empty states."
|
|
5
5
|
timestamp: 2026-10-12
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Guide: the Calendar tab of the console page
|
|
9
9
|
|
|
10
|
-
The Calendar tab of the console page (
|
|
10
|
+
The Calendar tab of the console page (`/`) shows time. It shows what happens and what falls due, by date. At the top it shows a short deck of headline cards about your calendar, for example a conflict or a meeting with no time to prepare. The deck shows nothing when PMOS has no card about your calendar. The tab shows no task list and no strategy panel. Your due items live in Work. The strategy and progress panels live in Reports and Initiatives.
|
|
11
11
|
|
|
12
12
|
## Calendar and Notes
|
|
13
13
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Guide
|
|
3
|
-
title: "Sessions in console3: open, read and end your sessions"
|
|
4
|
-
description: "How the Sessions panel in console3 groups your sessions, how to open a session's terminal without leaving console3, and how to end a session."
|
|
3
|
+
title: "Sessions in console3: start, open, read and end your sessions"
|
|
4
|
+
description: "How the Sessions panel in console3 groups your sessions, how to start a session, how to open a session's terminal without leaving console3, and how to end a session."
|
|
5
5
|
timestamp: 2026-10-12
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# Sessions in console3: open, read and end your sessions
|
|
8
|
+
# Sessions in console3: start, open, read and end your sessions
|
|
9
9
|
|
|
10
10
|
Press the **Sessions** button in the console3 header to open the panel. It is the bolt icon with no label. Point at it to see its name. When a session needs you, a count shows on it. It shows your sessions in four groups: **On a card**, **Started by me**, **Past (7 days)** and **Automatic**.
|
|
11
11
|
|
|
@@ -13,6 +13,24 @@ Press the **Sessions** button in the console3 header to open the panel. It is th
|
|
|
13
13
|
|
|
14
14
|
The dock is open on the Briefing. On the other tabs it is folded away, and no rail of buttons is left at the right edge. To open the dock again, press Ctrl J (⌘J on a Mac), or use **Open the dock** in the **More** menu.
|
|
15
15
|
|
|
16
|
+
## Start a session
|
|
17
|
+
|
|
18
|
+
You start a session in console3. You do not need the classic app.
|
|
19
|
+
|
|
20
|
+
1. Do one of these steps:
|
|
21
|
+
- Press **+** in the header of the dock.
|
|
22
|
+
- Press **+ session** on a branch that has no session.
|
|
23
|
+
- Press **New** next to **Started by me** in the Sessions panel.
|
|
24
|
+
2. The **New session** sheet opens. Type a name, or leave it empty to use `Shell` or `Claude`.
|
|
25
|
+
3. Select the **Agent**: Shell or Claude. A Claude session uses the profile, the launch flags and the tier from Settings.
|
|
26
|
+
4. Select where the session runs in **Runs on**:
|
|
27
|
+
- **Automatic**: the server selects the tree.
|
|
28
|
+
- A listed tree: the session runs in that working tree.
|
|
29
|
+
- **New branch**: type the branch name. The session runs in its own working copy of the new branch.
|
|
30
|
+
5. Press **Start**. The session starts, and its terminal opens in a sheet on the same page.
|
|
31
|
+
|
|
32
|
+
If the server cannot start the session, the sheet shows the reason.
|
|
33
|
+
|
|
16
34
|
## Read a row
|
|
17
35
|
|
|
18
36
|
Each row under **Started by me** shows the state of the session, how long ago it started, and its tree. The state comes from the workspace server, for example `working` or `quiet`.
|
package/workspace/guide/index.md
CHANGED
|
@@ -22,7 +22,7 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
|
|
|
22
22
|
| You want | Go to |
|
|
23
23
|
|---|---|
|
|
24
24
|
| What is waiting for you now | Agenda |
|
|
25
|
-
| Your day as a brief: today's meetings, what needs you, the portfolio | The Briefing page of the console (
|
|
25
|
+
| Your day as a brief: today's meetings, what needs you, the portfolio | The Briefing page of the console (`/`) |
|
|
26
26
|
| Every Jira ticket and WBS item assigned to you, ranked, to brief an agent | My work, or press `g w` |
|
|
27
27
|
| The initiatives of a repository, or of every repository | Initiatives. Choose **All repositories** in Repository ▾ for every repository. |
|
|
28
28
|
| The sessions of a repository, by initiative | The agent dock. ⌘K → **Branches and sessions** lists the branches. |
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Initiatives on the console: Portfolio, boards and workflows"
|
|
4
|
-
description: "How to use the Initiatives tab of the console page (/
|
|
4
|
+
description: "How to use the Initiatives tab of the console page (/): the Portfolio of boards, a board and its nine tabs, an initiative with its own workflow and stage records, and the Build and deliver setting."
|
|
5
5
|
timestamp: 2026-10-11
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Initiatives on the console: Portfolio, boards and workflows
|
|
9
9
|
|
|
10
|
-
The Initiatives tab of the console page (
|
|
10
|
+
The Initiatives tab of the console page (`/`) has these levels: **Portfolio** › **Board** › **Initiative** › **Epic** › **Item**. A breadcrumb at the top takes you back one level. The address bar keeps your place in the `init=` parameter, so a reload opens the same page.
|
|
11
11
|
|
|
12
12
|
## Portfolio
|
|
13
13
|
|
|
@@ -62,4 +62,4 @@ A project workflow has the launch checklist and the learnings only. If the works
|
|
|
62
62
|
|
|
63
63
|
## Demo mode
|
|
64
64
|
|
|
65
|
-
Open
|
|
65
|
+
Open `/?demo=1` to see five sample boards and about 310 initiatives. In demo mode nothing is saved, and a reload starts again.
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Guide: layout"
|
|
4
4
|
description: "The frame: one top bar, the place on screen, the detail and the agent dock; the phone tabs and lists; the keys; and the older layout."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-12
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Guide: layout
|
|
9
9
|
|
|
10
|
+
This page describes the classic workspace, at `/classic.html`. The address `/` opens the console (see [the Briefing](briefing.md)). The console's More menu has **Open the classic app**. You can start a session in the console too (see [Sessions in console3](console3-sessions.md)).
|
|
11
|
+
|
|
10
12
|
On a desktop screen the workspace is one frame. It has one top bar, the place on screen under it, the detail when you open one, and the agent dock at the right. There is no left sidebar.
|
|
11
13
|
|
|
12
14
|
## The top bar
|
|
@@ -102,7 +104,7 @@ When the workspace needs an answer from you, for example the name for a rename o
|
|
|
102
104
|
|
|
103
105
|
The workspace is neutral. A primary button is the text colour on the page colour, in both themes. The one brand colour marks the Agenda dot and the Agenda count, and nothing else. The bar, the dock and the bottom bar follow the theme; only a terminal stays dark. Corners are square. Controls are icons from one set, not text symbols. On a touch screen every field is 16 pixels, so the phone does not zoom when you tap it, and every control is at least 44 pixels high.
|
|
104
106
|
|
|
105
|
-
On the console page (
|
|
107
|
+
On the console page (`/`), text on the blue accent and on a red badge is white in both themes. The page takes this colour from one token, so the colours stay the same when the theme changes.
|
|
106
108
|
|
|
107
109
|
## Keys
|
|
108
110
|
|
|
@@ -111,7 +113,7 @@ On the console page (`/c3.html`), text on the blue accent and on a red badge is
|
|
|
111
113
|
| g t, g i, g b, g r, g d, g p | Show Agenda, Initiatives, Board, Reports, Docs or People. |
|
|
112
114
|
| ⌘K (Ctrl-K) | Open or close the command palette. |
|
|
113
115
|
| ⌘J (Ctrl-J) | Open or fold the dock. |
|
|
114
|
-
| C | On the console page (
|
|
116
|
+
| C | On the console page (`/`): open the [calendar popup](calendar-popup.md). Ignored while you type or while a sheet is open. |
|
|
115
117
|
| ⌘⇧J (Ctrl-Shift-J) | Focus: give the dock the whole window, or show the place again. |
|
|
116
118
|
| j, k | Move down or up the rows of the place. |
|
|
117
119
|
| Enter | Open the row that has the focus. |
|
package/workspace/guide/notes.md
CHANGED
|
@@ -22,7 +22,7 @@ Starting a note, from the Record control or from **Take notes** on a calendar ev
|
|
|
22
22
|
|
|
23
23
|
### The Record control in the console
|
|
24
24
|
|
|
25
|
-
The console (the page at
|
|
25
|
+
The console (the page at `/`) has the same Record control in its top bar, beside the Sessions button. It is always there, also when nothing is recording. **Record** (the microphone) starts an untitled note and listens at once. The arrow lists the meetings of today that have not ended, including one that has started, until 10 minutes after its end. Press one to start a note linked to that calendar event. While a meeting records, the red group shows its name and clock, **Highlight**, **Pause** and **Stop**. While it is paused, the group shows **Resume** and **Stop**. **Stop** asks first and offers **Pause instead**. A red dot shows on the Calendar tab while a meeting is held. Press the name in the red group to open the note on the console's Calendar tab, in **Notes**, on the **During** stage. See [the Calendar tab of the console page](console-calendar.md).
|
|
26
26
|
|
|
27
27
|
When Whisper or Qwen3-ASR transcribes the meeting, the red group also names the engine and the language, for example **Whisper · English**. Check it when lines come out in the wrong language: the language is set in the Notes settings.
|
|
28
28
|
|
|
@@ -7,7 +7,7 @@ timestamp: 2026-10-12
|
|
|
7
7
|
|
|
8
8
|
# Settings in console3: one home for each setting
|
|
9
9
|
|
|
10
|
-
Settings is the page in console3 (
|
|
10
|
+
Settings is the page in console3 (`/`) where you change how PMOS works. Each setting has one home on this page. When another page shows a setting, it shows a link to this page and not a second control.
|
|
11
11
|
|
|
12
12
|
## Open Settings
|
|
13
13
|
|
|
@@ -16,7 +16,7 @@ Use one of these:
|
|
|
16
16
|
1. Press the gear in the top bar, next to More.
|
|
17
17
|
2. Press More, then **Settings**.
|
|
18
18
|
3. Press ⌘K, type `settings`, and press Enter.
|
|
19
|
-
4. Open
|
|
19
|
+
4. Open `/?tab=settings`. Add `§ion=recording` to open one section, for example.
|
|
20
20
|
|
|
21
21
|
Settings is not a tab. While it is open, no tab is selected. Press a tab to go back to that tab.
|
|
22
22
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.161.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.161.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
package/workspace/public/c3.html
CHANGED
|
@@ -5,8 +5,17 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
6
6
|
<meta name="pmos-nonce" content="" />
|
|
7
7
|
<title>PMOS console</title>
|
|
8
|
-
<!--
|
|
9
|
-
|
|
8
|
+
<!-- Add to Home Screen, as the older shell had it (see index.html): the manifest is fetched with credentials because over the
|
|
9
|
+
tailnet every request needs the session cookie. -->
|
|
10
|
+
<link rel="manifest" href="/manifest.webmanifest" crossorigin="use-credentials" />
|
|
11
|
+
<meta name="theme-color" content="#090b0c" />
|
|
12
|
+
<meta name="mobile-web-app-capable" content="yes" />
|
|
13
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
14
|
+
<meta name="apple-mobile-web-app-title" content="PMOS" />
|
|
15
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
|
16
|
+
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
|
|
17
|
+
<!-- The console3 shell (header, tabs, three columns, dock), served as the default page at / (and still at /c3.html). It
|
|
18
|
+
composes the real views from pm/; the older shell (index.html) is served at /classic.html. Tokens and fonts are shared, so light and dark both resolve. pm/c3-shell.js sets the
|
|
10
19
|
theme attribute (default light, like the mockup). The nonce meta is filled by the server the way it is for /, so a press
|
|
11
20
|
from a view (Add, Accept, Allow) carries it. -->
|
|
12
21
|
<link rel="preload" href="/vendor/ibm-plex-mono-400.woff2" as="font" type="font/woff2" crossorigin />
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* pm/c3-dock.js — the console3 dock on /c3.html: the mockup's repository, branch and session tree (console3.html renderDock, line 1957).
|
|
2
|
-
* Header: the repository select,
|
|
2
|
+
* Header: the repository select, the New session button (c3-new-session.js) and the fold button. Tree: one .br per working tree of the repository, its branch on
|
|
3
3
|
* top and the sessions that run in that tree under it, each with a state glyph, a provider icon and a name. Below the tree: the
|
|
4
4
|
* selected session's detail (what it is, where it runs, how long, the output tail, the Message box) and a scratchpad.
|
|
5
5
|
* Real data only:
|
|
@@ -17,6 +17,10 @@ import { c3ic } from './pm-work-view.js';
|
|
|
17
17
|
import { ic, TIPS } from './c3-terminal-icons.js';
|
|
18
18
|
import { source, needsYou } from './c3-terminal-data.js';
|
|
19
19
|
import { toast } from './c3-toast.js';
|
|
20
|
+
import { button } from './primitives.js';
|
|
21
|
+
|
|
22
|
+
/* a primitives button (keyboard, role) carrying the dock's own look and children */
|
|
23
|
+
const c3btn = (attrs, onClick, ...kids) => { const b = button({ variant: 'ghost', attrs, onClick }); b.replaceChildren(...kids); return b; };
|
|
20
24
|
|
|
21
25
|
/* the mockup's SM (line 1953): glyph and class by state. The server's `shown` words map onto them. */
|
|
22
26
|
const SM = { working: ['●', 'w'], needs: ['◆', 'n'], quiet: ['◼', ''], blocked: ['◇', 'b'], exited: ['○', ''] };
|
|
@@ -24,8 +28,8 @@ const smOf = (s) => SM[s.shown] || SM[s.state] || ['●', ''];
|
|
|
24
28
|
const shownOf = (s, ended) => (ended && ended.has(s.id) ? 'exited' : s.shown);
|
|
25
29
|
const PI = { claude: 'term', cloud: 'cloud', codex: 'bolt', shell: 'term', local: 'term' }; // spark is reserved for the agent surfaces
|
|
26
30
|
const PN = { claude: 'Claude', cloud: 'Claude cloud', codex: 'Codex', shell: 'Shell', local: 'Local' };
|
|
27
|
-
const NOEND = 'Ending a session needs the page route /api/session/remove, which did not answer. Use the terminal in the
|
|
28
|
-
const MORE = 'Sending a prompt is not available on this page: no safe route sends one. Use the session\'s terminal in the
|
|
31
|
+
const NOEND = 'Ending a session needs the page route /api/session/remove, which did not answer. Use the terminal in the classic app (/classic.html).';
|
|
32
|
+
const MORE = 'Sending a prompt is not available on this page: no safe route sends one. Use the session\'s terminal in the classic app (/classic.html).';
|
|
29
33
|
const MAX_TREES = 10;
|
|
30
34
|
|
|
31
35
|
const ago = (ms) => {
|
|
@@ -50,13 +54,15 @@ export function createDock(opts) {
|
|
|
50
54
|
const side = el('aside', { class: 'c3dock dock', 'aria-label': 'Sessions', 'data-c3pg': 'dock' },
|
|
51
55
|
el('div', { class: 'dk-h' }, c3ic('repo', 'sm'), sel,
|
|
52
56
|
listBtn,
|
|
53
|
-
|
|
57
|
+
c3btn({ class: 'ib sm', title: 'New session', 'aria-label': 'New session', 'data-c3pg': 'dock-new' }, () => newSession(), c3ic('plus', 'sm')),
|
|
54
58
|
el('button', { type: 'button', class: 'ib sm', title: 'Fold the dock (Ctrl J)', 'aria-label': 'Fold the dock', onclick: () => o.setOpen && o.setOpen(false) }, c3ic('fold'))),
|
|
55
59
|
body);
|
|
56
60
|
|
|
57
61
|
const withEnd = (s) => (S.ended.has(s.id) ? { ...s, shown: 'exited', label: 'ended' } : s);
|
|
58
62
|
const mine = () => S.sessions.filter((s) => s.ws === S.ws && !s.automatic).map(withEnd);
|
|
59
63
|
const inTree = (dir) => mine().filter((s) => s.tree === dir).sort((a, b) => (a.shown === 'exited') - (b.shown === 'exited') || String(b.created).localeCompare(String(a.created)));
|
|
64
|
+
/* a new session: the sheet asks name, agent and where it runs, starts it over the terminal connection and opens its terminal here (c3-new-session) */
|
|
65
|
+
const newSession = (on) => import('./c3-new-session.js').then((m) => m.openNewSession({ ws: S.ws, trees: (S.trees || []).filter((t) => t.ws === S.ws || t.main), on, onStarted: (id) => { S.sel = id; refresh(); } }));
|
|
60
66
|
const pick = (id) => { S.sel = id; S.selTree = null; S.confirm = null; drawTree(); drawDetail(); if (o.setOpen) o.setOpen(true); };
|
|
61
67
|
|
|
62
68
|
function row(s) {
|
|
@@ -73,7 +79,7 @@ export function createDock(opts) {
|
|
|
73
79
|
const ss = inTree(t.dir);
|
|
74
80
|
const body = ss.length ? ss.slice(0, 4).map(row)
|
|
75
81
|
: [t.main ? el('span', { class: 'dim mono', style: 'font-size:var(--fs-1);padding:2px 6px' }, 'no session')
|
|
76
|
-
:
|
|
82
|
+
: c3btn({ class: 'chip2 lk', title: 'Start a session on this branch', 'data-c3pg': 'dock-new-here' }, () => newSession('ws:' + t.ws), c3ic('plus', 'sm'), 'session')];
|
|
77
83
|
if (ss.length > 4) body.push(el('span', { class: 'dim mono', style: 'font-size:var(--fs-1);padding:2px 6px' }, '+' + (ss.length - 4) + ' more'));
|
|
78
84
|
return el('div', { class: 'br', 'data-c3-tree': t.dir }, el('button', { type: 'button', class: 'bn', 'data-c3-branch': t.dir, 'aria-current': String(!!S.sel && ss.some((x) => x.id === S.sel)), title: ss.length ? 'Open the session on this branch' : 'This branch has no session', onclick: () => { if (ss[0]) pick((ss.find((x) => x.shown !== 'exited') || ss[0]).id); else { S.sel = null; S.selTree = t.dir; drawTree(); drawDetail(); } },
|
|
79
85
|
style: 'border:0;background:none;text-align:left;width:100%;cursor:pointer' }, c3ic('branch', 'sm'), t.branch || '(detached)'), el('div', { class: 'ss' }, ...body));
|
|
@@ -82,13 +88,13 @@ export function createDock(opts) {
|
|
|
82
88
|
const known = new Set(list.map((t) => t.dir));
|
|
83
89
|
const stray = mine().filter((s) => !known.has(s.tree));
|
|
84
90
|
if (stray.length) nodes.push(el('div', { class: 'br' }, el('div', { class: 'bn' }, c3ic('branch', 'sm'), 'other trees'), el('div', { class: 'ss' }, ...stray.slice(0, 4).map(row))));
|
|
85
|
-
if (all.length > MAX_TREES) nodes.push(el('div', { class: 'dim mono
|
|
91
|
+
if (all.length > MAX_TREES) nodes.push(el('div', { class: 'dim mono dk-more' }, '+' + (all.length - MAX_TREES) + ' more branches in the classic app'));
|
|
86
92
|
tree.replaceChildren(...(nodes.length ? nodes : [el('p', { class: 'dim mono', style: 'font-size:var(--fs-2);padding:4px' }, 'No working tree is registered for this repository.')]));
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
async function drawDetail() {
|
|
90
96
|
const s = mine().find((x) => x.id === S.sel);
|
|
91
|
-
if (!s) { const bt = S.selTree && S.trees && S.trees.find((x) => x.dir === S.selTree); detail.replaceChildren(el('p', { class: 'dim mono
|
|
97
|
+
if (!s) { const bt = S.selTree && S.trees && S.trees.find((x) => x.dir === S.selTree); detail.replaceChildren(el('p', { class: 'dim mono dk-empty', 'data-c3pg': 'dock-empty' }, bt ? (bt.branch || 'This branch') + ' has no session. Press + session to start one.' : mine().length ? 'Pick a session above.' : 'No session runs in this repository.')); return; }
|
|
92
98
|
const t = S.trees && S.trees.find((x) => x.dir === s.tree);
|
|
93
99
|
const started = ago(Date.now() - Date.parse(s.created));
|
|
94
100
|
const last = Number.isFinite(s.lastOutputAt) ? ago(Date.now() - s.lastOutputAt) : null;
|
|
@@ -103,7 +109,7 @@ export function createDock(opts) {
|
|
|
103
109
|
el('div', { class: 'chips2' }, el('b', { style: 'font-size:var(--fs-3)' }, s.name), el('span', { class: 'chips2 dim mono', style: 'font-size:var(--fs-1)' }, c3ic(PI[s.agent] || 'term', 'sm'), (PN[s.agent] || s.agent) + ' · ' + ((t && t.branch) || 'no branch'))),
|
|
104
110
|
el('div', { class: 'dim mono', style: 'font-size:var(--fs-1)' }, facts),
|
|
105
111
|
s.shown === 'needs' ? el('div', { class: 'fold a' }, c3ic('alert', 'sm'), 'needs you') : null,
|
|
106
|
-
el('a', { class: 'chip2 lk', href: '/', 'data-c3pg': 'dock-open-session', title: TIPS.terminalOpen, 'aria-label': 'Open the session ' + s.name,
|
|
112
|
+
el('a', { class: 'chip2 lk', href: '/classic.html', 'data-c3pg': 'dock-open-session', title: TIPS.terminalOpen, 'aria-label': 'Open the session ' + s.name,
|
|
107
113
|
onclick: (e) => { if (e.metaKey || e.ctrlKey || e.shiftKey || e.button > 0) return; e.preventDefault(); import('./c3-session-term.js').then((m) => m.openSessionTerm({ id: s.id, name: s.name })); } }, ic('term', 'sm'), 'Open the session'),
|
|
108
114
|
term,
|
|
109
115
|
el('div', { class: 'form' },
|
|
@@ -169,6 +175,7 @@ export function createDock(opts) {
|
|
|
169
175
|
start() { document.addEventListener('keydown', onKey); drawTree(); refresh(); timer = setInterval(refresh, 15000); if (source().subscribe) S.unsub = source().subscribe(refresh); },
|
|
170
176
|
stop() { if (timer) clearInterval(timer); if (S.unsub) S.unsub(); document.removeEventListener('keydown', onKey); },
|
|
171
177
|
refresh,
|
|
178
|
+
newSession,
|
|
172
179
|
};
|
|
173
180
|
return api;
|
|
174
181
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/* pm/c3-new-session.js — start a session from console3, in a sheet, and open its terminal in the console (c3-new-session).
|
|
2
|
+
* openNewSession({ ws, trees, initiative, on, onStarted }) on: 'auto' (default), 'ws:<id>' (that tree) or 'new'
|
|
3
|
+
* Before this, every "start a session" press on console3 went to the classic app (/classic.html), because the page had no way
|
|
4
|
+
* to start one. A session starts over the terminal connection, so this uses the same request as the classic dock's new-session
|
|
5
|
+
* sheet (tab-term.js requestSpawn) and then opens the terminal with c3-session-term.js. Nothing opens unless the server says the
|
|
6
|
+
* session started; a refusal stays in the sheet as the server's sentence.
|
|
7
|
+
* Fields: name (empty: Shell or Claude, numbered when taken), agent (the Settings default first), and where it runs:
|
|
8
|
+
* Automatic the server's start policy picks the tree (the initiative's own working copy, or the main tree)
|
|
9
|
+
* a listed tree that working tree as it is
|
|
10
|
+
* New branch a new branch in its own working copy
|
|
11
|
+
* A Claude session takes the server's default profile and the Settings launch flags and tier, as the classic dock does. */
|
|
12
|
+
import { el } from './dom.js';
|
|
13
|
+
import { openSheet } from './c3-sheet.js';
|
|
14
|
+
import { button } from './primitives.js';
|
|
15
|
+
import { toast } from './c3-toast.js';
|
|
16
|
+
import { pmGet } from './http.js';
|
|
17
|
+
import { sessionDefaults, adoptProfileDefault, CLIS } from './dock.js';
|
|
18
|
+
|
|
19
|
+
const AGENT = { shell: 'Shell', claude: 'Claude' };
|
|
20
|
+
|
|
21
|
+
/** A free name: `base`, else `base 2`, `base 3`, … among the sessions that have not ended. Pure, for Node. */
|
|
22
|
+
export function freeName(base, sessions) {
|
|
23
|
+
const taken = new Set((sessions || []).filter((s) => s && s.state !== 'exited' && s.shown !== 'exited').map((s) => s.name));
|
|
24
|
+
if (!taken.has(base)) return base;
|
|
25
|
+
for (let i = 2; ; i += 1) if (!taken.has(base + ' ' + i)) return base + ' ' + i;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** What requestSpawn is sent, from the form. Pure, for Node. Returns { error } when the form is not complete. */
|
|
29
|
+
export function spawnOpts(f, d) {
|
|
30
|
+
const claude = f.agent === 'claude';
|
|
31
|
+
const common = {
|
|
32
|
+
name: f.name, agent: f.agent, initiative: f.initiative || null,
|
|
33
|
+
profile: claude ? (f.profile === undefined ? null : f.profile) : null,
|
|
34
|
+
flags: claude && d && Array.isArray(d.flags) && d.flags.length ? d.flags.map((x) => ({ flag: x.flag, ...(x.value ? { value: x.value } : {}) })) : undefined,
|
|
35
|
+
tier: claude && d && d.tier && d.tier !== 'auto' ? d.tier : null,
|
|
36
|
+
workType: 'general',
|
|
37
|
+
};
|
|
38
|
+
if (f.on === 'new') {
|
|
39
|
+
const b = String(f.branch || '').trim();
|
|
40
|
+
if (!b) return { error: 'Type a name for the new branch.' };
|
|
41
|
+
return { ...common, ws: f.ws, branch: b, createBranch: true, isolate: true };
|
|
42
|
+
}
|
|
43
|
+
if (f.on && f.on.startsWith('ws:')) return { ...common, ws: f.on.slice(3), branch: null, createBranch: false, isolate: null };
|
|
44
|
+
return { ...common, ws: f.ws, start: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function openNewSession(opts) {
|
|
48
|
+
const o = opts || {};
|
|
49
|
+
const d = sessionDefaults();
|
|
50
|
+
const trees = (o.trees || []).filter((t) => t && !t.missing && t.ws);
|
|
51
|
+
const F = { agent: CLIS.includes(d.cli) ? d.cli : 'shell', on: o.on && (o.on === 'new' || trees.some((t) => 'ws:' + t.ws === o.on)) ? o.on : 'auto', busy: false };
|
|
52
|
+
|
|
53
|
+
const name = el('input', { type: 'text', 'aria-label': 'Name', placeholder: 'Empty: Shell or Claude', 'data-c3ns': 'name' });
|
|
54
|
+
const agent = el('select', { 'aria-label': 'Agent', 'data-c3ns': 'agent', onchange: (e) => { F.agent = e.target.value; } },
|
|
55
|
+
...CLIS.map((c) => el('option', { value: c, selected: c === F.agent }, AGENT[c] || c)));
|
|
56
|
+
const branch = el('input', { type: 'text', 'aria-label': 'New branch', placeholder: 'feature/…', 'data-c3ns': 'branch' });
|
|
57
|
+
const branchRow = el('label', { class: 'c3ns-f', hidden: F.on !== 'new' }, el('span', { class: 'dim mono' }, 'New branch'), branch);
|
|
58
|
+
const where = el('select', { 'aria-label': 'Runs on', 'data-c3ns': 'on', onchange: (e) => { F.on = e.target.value; branchRow.hidden = F.on !== 'new'; } },
|
|
59
|
+
el('option', { value: 'auto', selected: F.on === 'auto' }, o.initiative ? 'Automatic: the working copy of initiative/' + o.initiative : 'Automatic: the main working tree'),
|
|
60
|
+
...trees.map((t) => el('option', { value: 'ws:' + t.ws, selected: F.on === 'ws:' + t.ws }, (t.main ? 'Main tree' : '⑂') + ' ' + (t.branch || 'detached'))),
|
|
61
|
+
el('option', { value: 'new', selected: F.on === 'new' }, 'New branch'));
|
|
62
|
+
const err = el('p', { class: 'c3ns-err', role: 'alert', 'data-c3ns': 'error' });
|
|
63
|
+
const go = button({ label: 'Start', variant: 'primary', size: 'sm', onClick: () => start(), attrs: { 'data-c3ns': 'start' } });
|
|
64
|
+
const body = el('div', { class: 'c3ns', 'data-c3ns': 'form' },
|
|
65
|
+
el('label', { class: 'c3ns-f' }, el('span', { class: 'dim mono' }, 'Name'), name),
|
|
66
|
+
el('label', { class: 'c3ns-f' }, el('span', { class: 'dim mono' }, 'Agent'), agent),
|
|
67
|
+
el('label', { class: 'c3ns-f' }, el('span', { class: 'dim mono' }, 'Runs on'), where),
|
|
68
|
+
branchRow, err, el('div', { class: 'c3ns-go' }, go));
|
|
69
|
+
const sheet = openSheet({ title: o.initiative ? 'New session for ' + o.initiative : 'New session', icon: 'term', body });
|
|
70
|
+
requestAnimationFrame(() => name.focus());
|
|
71
|
+
|
|
72
|
+
async function start() {
|
|
73
|
+
if (F.busy) return;
|
|
74
|
+
if (!o.ws) { err.textContent = 'Pick a repository first.'; return; }
|
|
75
|
+
F.busy = true; go.disabled = true; err.textContent = '';
|
|
76
|
+
const sessions = await pmGet('/api/sessions').then((r) => (r && Array.isArray(r.sessions) ? r.sessions : []), () => []);
|
|
77
|
+
const base = (AGENT[F.agent] || 'Shell') + (o.initiative ? ' · ' + o.initiative : '');
|
|
78
|
+
// the default profile is the server's; this browser's copy can be old, so a Claude start reads it first
|
|
79
|
+
const profile = F.agent === 'claude' ? adoptProfileDefault(await pmGet('/api/profiles').catch(() => null)) : null;
|
|
80
|
+
const req = spawnOpts({ name: name.value.trim() || freeName(base, sessions), agent: F.agent, initiative: o.initiative, profile, on: F.on, branch: branch.value, ws: o.ws }, d);
|
|
81
|
+
if (req.error) { err.textContent = req.error; F.busy = false; go.disabled = false; return; }
|
|
82
|
+
const term = await import('./tab-term.js').catch(() => null);
|
|
83
|
+
const res = term ? await term.requestSpawn(req) : { ok: false, error: 'The terminal connection could not be loaded on this page.' };
|
|
84
|
+
F.busy = false; go.disabled = false;
|
|
85
|
+
if (!res || !res.ok) { err.textContent = (res && res.error) || 'The session was not started.'; return; }
|
|
86
|
+
sheet.close();
|
|
87
|
+
if (o.onStarted) o.onStarted(res.id);
|
|
88
|
+
const m = await import('./c3-session-term.js');
|
|
89
|
+
m.openSessionTerm({ id: res.id, name: req.name });
|
|
90
|
+
// a session that shares its working tree with a running one is told so: their edits land in the same files
|
|
91
|
+
if (res.notice && /already running/.test(res.notice)) toast({ text: res.notice, timeout: 12000 });
|
|
92
|
+
}
|
|
93
|
+
return sheet;
|
|
94
|
+
}
|
|
@@ -143,7 +143,7 @@ export function newInitLine(ctx) {
|
|
|
143
143
|
call.then((out) => { if (me !== run) return; slot.replaceChildren(out && out.ok && out.draft ? answer(text, out.draft) : el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), (out && out.error) || 'The draft did not answer.')); }); }, 650);
|
|
144
144
|
};
|
|
145
145
|
input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.isComposing) start(input.value); });
|
|
146
|
-
const mic = c3ib('Speak is off on this line: on-device dictation runs in Notes, in the
|
|
146
|
+
const mic = c3ib('Speak is off on this line: on-device dictation runs in Notes, in the classic app (/classic.html). Type the line here.', 'mic', { disabled: true });
|
|
147
147
|
const spark = c3ib('Start: an agent runs the intake', 'spark', { 'data-a': 'c3nigo', 'data-c3pg': 'new-init-go', onClick: () => start(input.value) }, 'pri');
|
|
148
148
|
return el('div', null, el('div', { class: 'c3add' }, el('div', { class: 'form' }, input, mic, spark),
|
|
149
149
|
el('div', { class: 'chips2' }, el('span', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, 'try'),
|
|
@@ -570,9 +570,9 @@ export function mountSettings(host, ctx = {}) {
|
|
|
570
570
|
return [group('New sessions', [
|
|
571
571
|
extRow('sess.cli', seg('Start with', [['claude', 'Claude Code'], ['shell', 'A shell']], d.cli, (v) => setDef({ cli: v }, 'sess.cli', { cli: d.cli })), { scope: 'computer', note: 'Kept in this browser, as the dock keeps it.' }),
|
|
572
572
|
extRow('sess.tier', seg('Model', [['auto', 'Auto'], ['fast', 'Fast'], ['balanced', 'Balanced'], ['deep', 'Deep']], d.tier, (v) => setDef({ tier: v }, 'sess.tier', { tier: d.tier })), { scope: 'computer' }),
|
|
573
|
-
extRow('sess.profile', profileSelect('c3set-sess-profile'), { scope: 'computer', forId: 'c3set-sess-profile', note: profs.length ? null : p ? 'No profile was found. Make one in the
|
|
573
|
+
extRow('sess.profile', profileSelect('c3set-sess-profile'), { scope: 'computer', forId: 'c3set-sess-profile', note: profs.length ? null : p ? 'No profile was found. Make one in the classic app\'s Settings, Profiles (/classic.html).' : 'Reading profiles.' }),
|
|
574
574
|
lockedRow('sess.flags', 'Launch flags', 'Flags for the claude command, such as a permission mode.', 'Set when you start a session', 'computer')], 'term'),
|
|
575
|
-
profs.length ? sect('Profiles', [list(profRows), help('A new profile is made in the
|
|
575
|
+
profs.length ? sect('Profiles', [list(profRows), help('A new profile is made in the classic app\'s Settings, Profiles (/classic.html).')], { ic: 'user', count: profs.length }) : null,
|
|
576
576
|
groupOf('sessions', 'Housekeeping', 'term'),
|
|
577
577
|
sect('Running now' + (M.running ? ' · ' + run.length + (max ? ' of ' + max : '') : ''), [M.running ? (run.length ? list(runRows) : fold(null, 'No sessions running.')) : fold(null, 'Reading the sessions.'),
|
|
578
578
|
help('Close a session from the dock at the bottom of the page.')], { ic: 'term' }),
|
|
@@ -136,7 +136,7 @@ function toggleMore() {
|
|
|
136
136
|
item(S.theme === 'dark' ? 'sun' : 'moon', S.theme === 'dark' ? 'Light theme' : 'Dark theme', () => setTheme(S.theme === 'dark' ? 'light' : 'dark')),
|
|
137
137
|
((b) => { b.firstChild.replaceWith(gearIcon('sm')); return b; })(item('info', 'Settings', () => showTab('settings', true), false, { 'data-a': 'settings' })),
|
|
138
138
|
item('term', S.dockOpen ? 'Fold the dock' : 'Open the dock', () => { S.dockOpen = !S.dockOpen; syncDock(); }, false, { 'data-a': 'dockt' }, 'Ctrl J'),
|
|
139
|
-
el('a', { class: 'c3pg-mi', role: 'menuitem', href: '/' }, c3ic('term', 'sm'), el('span', null, 'Open the
|
|
139
|
+
el('a', { class: 'c3pg-mi', role: 'menuitem', href: '/classic.html' }, c3ic('term', 'sm'), el('span', null, 'Open the classic app')));
|
|
140
140
|
S.root.append(S.menu); // on the page root with position:fixed, so no ancestor's overflow, transform or stacking order can hide it
|
|
141
141
|
S.placeMenu = () => { const r = S.moreBtn.getBoundingClientRect(); const w = S.menu.offsetWidth || 220; S.menu.style.top = Math.round(r.bottom + 6) + 'px'; S.menu.style.left = Math.round(Math.max(8, Math.min(r.right - w, innerWidth - w - 8))) + 'px'; S.menu.style.maxHeight = Math.max(160, innerHeight - r.bottom - 14) + 'px'; };
|
|
142
142
|
S.placeMenu(); addEventListener('resize', S.placeMenu); addEventListener('scroll', S.placeMenu, true);
|
|
@@ -425,7 +425,9 @@ function dock() {
|
|
|
425
425
|
});
|
|
426
426
|
S.dockEl = S.dock.side;
|
|
427
427
|
// a session opens in its own terminal sheet on this page (c3-session-term.js), not in the main app
|
|
428
|
-
|
|
428
|
+
// no id means "start one": the new-session sheet opens on this page (c3-new-session.js), not the classic app
|
|
429
|
+
window.addEventListener('pmos:open-session', (e) => { const id = e.detail && e.detail.id; if (!id) { S.dock.newSession(); return; } import('./c3-session-term.js').then((m) => m.openSessionTerm({ id, name: e.detail.name })); });
|
|
430
|
+
window.addEventListener('pmos:new-session', (e) => { e.preventDefault(); S.dock.newSession(e.detail && e.detail.on); });
|
|
429
431
|
return [S.dock.side];
|
|
430
432
|
}
|
|
431
433
|
/* The dock is open on the Briefing and folded away on the other tabs. A folded dock leaves no rail (PM, 2026-10-12): the header's Sessions button, Ctrl J and the More menu reach it. */
|
|
@@ -491,7 +493,7 @@ function showInit(id) {
|
|
|
491
493
|
S.initHosts[k].hidden = k !== id;
|
|
492
494
|
if (k === id && !S.mounted['init-' + k] && S.initRows) {
|
|
493
495
|
S.mounted['init-' + k] = true;
|
|
494
|
-
const ctx = { ...initiativesViewCtx({ repo: ws.root || null, ws: S.ws, rows: S.initRows.rows, sig: S.initRows.sig, openInitiative: () => location.assign('/'), setState: () => {} }), strategy: S.initRows.strategy, plan: planNode(sc(OWN.has(k) ? 'init-' + k : 'init')), links: crossLinks((t) => showTab(t, true)) };
|
|
496
|
+
const ctx = { ...initiativesViewCtx({ repo: ws.root || null, ws: S.ws, rows: S.initRows.rows, sig: S.initRows.sig, openInitiative: () => location.assign('/classic.html'), setState: () => {} }), strategy: S.initRows.strategy, plan: planNode(sc(OWN.has(k) ? 'init-' + k : 'init')), links: crossLinks((t) => showTab(t, true)) };
|
|
495
497
|
if (k === 'lifecycle' || OWN.has(k)) mount(S.initHosts[k], Object.freeze(ctx));
|
|
496
498
|
else { const view = el('div'); S.initHosts[k].replaceChildren(ctx.plan, view); mount(view, Object.freeze({ ...ctx, plan: null })); }
|
|
497
499
|
}
|
|
@@ -115,7 +115,7 @@ function render(V) {
|
|
|
115
115
|
const reading = !D;
|
|
116
116
|
const none = !reading && !all.length;
|
|
117
117
|
const head = reading ? { icon: 'check', h: 'Reading the Jira tickets', s: 'Tickets live in Jira. The agent proposes, you accept.', ring: [0, 1] }
|
|
118
|
-
: none ? { icon: 'check', h: 'No Jira tickets read yet', s: D.err || 'The agent has not read Jira yet. A read from My work in the
|
|
118
|
+
: none ? { icon: 'check', h: 'No Jira tickets read yet', s: D.err || 'The agent has not read Jira yet. A read from My work in the classic app (/classic.html) fills this view.', ring: [0, 1] }
|
|
119
119
|
: { icon: 'check', h: T.length + ' Jira ticket' + (T.length === 1 ? '' : 's') + ', ' + bad.length + ' late or at risk', s: 'Tickets live in Jira. The agent proposes, you accept.', ring: [done.length, T.length] };
|
|
120
120
|
|
|
121
121
|
const left = [];
|
|
@@ -2215,3 +2215,14 @@
|
|
|
2215
2215
|
.c3.c3pg .c3set .st-hd input[type=text]{flex:1 1 100%;width:auto}
|
|
2216
2216
|
.c3.c3pg .c3set .st-hd select{flex:1 1 0;min-width:0}
|
|
2217
2217
|
}
|
|
2218
|
+
/* the dock's note under the branch list and its empty detail (c3-dock.js) */
|
|
2219
|
+
.c3dock .dk-more { font-size: var(--fs-1); padding: 4px; }
|
|
2220
|
+
.c3dock .dk-empty { font-size: var(--fs-2); }
|
|
2221
|
+
|
|
2222
|
+
/* the new-session sheet (c3-new-session.js) */
|
|
2223
|
+
.c3ns { display: flex; flex-direction: column; gap: var(--sp-3); }
|
|
2224
|
+
.c3ns-f { display: flex; flex-direction: column; gap: var(--sp-1); }
|
|
2225
|
+
.c3ns-f input, .c3ns-f select { width: 100%; }
|
|
2226
|
+
.c3ns-err { color: var(--destructive); font-size: var(--fs-2); margin: 0; }
|
|
2227
|
+
.c3ns-err:empty { display: none; }
|
|
2228
|
+
.c3ns-go { display: flex; justify-content: flex-end; }
|
|
@@ -122,7 +122,7 @@ export const COPY = Object.freeze({
|
|
|
122
122
|
empty: 'Nothing here.', noJobGroup: 'No job yet', jobs: 'Jobs to be done', noJobs: 'No jobs yet.',
|
|
123
123
|
addJob: 'Add a job', addJobPlaceholder: 'when I plan a release, I want honest dates, so Dana can trust them',
|
|
124
124
|
groupJob: 'Group by job to be done', groupTime: 'Group by time', loading: 'Reading…',
|
|
125
|
-
micOff: 'Speak is off on this line: on-device dictation runs in Notes, in the
|
|
125
|
+
micOff: 'Speak is off on this line: on-device dictation runs in Notes, in the classic app (/classic.html). Type the item here.',
|
|
126
126
|
jobsNote: 'The count is every item in the job, across all views. Tap a job to edit or delete it.',
|
|
127
127
|
legend: ['open', 'done'], readError: 'Your items could not be read.', saveError: 'Nothing was saved.',
|
|
128
128
|
removed: 'Removed.', undo: 'Undo', restored: 'Restored.', tooLate: 'Too late to undo.', drop: 'Drop',
|
|
@@ -130,10 +130,15 @@ function draw(host, st) {
|
|
|
130
130
|
: none('None running. A result appears on its card.')));
|
|
131
131
|
/* Started by me */
|
|
132
132
|
const mine = g.startedByMe || [];
|
|
133
|
-
|
|
133
|
+
// console3 answers pmos:new-session with its new-session sheet; a page that does not (the classic app) opens its own dock sheet
|
|
134
|
+
const add = button({ variant: 'ghost', attrs: { class: 'chip2 lk c3t-new', 'data-c3t': 'new', title: 'Start a new session' }, onClick: () => {
|
|
135
|
+
const ev = new CustomEvent('pmos:new-session', { cancelable: true, detail: {} });
|
|
136
|
+
if (window.dispatchEvent(ev)) Promise.all([import('./dock-sessions.js'), import('./board-read.js')]).then(([ds, br]) => ds.openNewSessionSheet(br.currentProduct(), { initiative: null })).catch(() => {});
|
|
137
|
+
} });
|
|
138
|
+
add.replaceChildren(ic('plus', 'sm'), 'New');
|
|
134
139
|
root.append(section('term', 'Started by me', mine.length, add, mine.length
|
|
135
140
|
? el('ul', { class: 'list', 'data-c3t': 'mine' }, mine.map((s) => { const c = cards[s.id]; return row(st, s, c, { sub: liveWord(s, c) + ' · ' + ago(s.startedAt) + ' · main tree', body: [plain(s, c) ? null : cardHost(st, s, c || { state: 'working', title: 'Working', nextMoves: [], claims: [], steps: [] }), blankPanel(st, s, c)].filter(Boolean) }); }))
|
|
136
|
-
: none('None.
|
|
141
|
+
: none('None. Press New to start one.')));
|
|
137
142
|
/* Past */
|
|
138
143
|
const past = g.past || [];
|
|
139
144
|
root.append(section('clock', 'Past (7 days)', past.length, null, past.length
|
|
@@ -147,7 +147,7 @@ const RGL = { highlighter: () => c3ic('flag', 'sm'), pause: () => glyph('M8 5v14
|
|
|
147
147
|
* elsewhere the main app, where the full note is. */
|
|
148
148
|
const openNote = (id) => {
|
|
149
149
|
if (document.querySelector('[data-c3-tab="cal"]')) { window.dispatchEvent(new CustomEvent('c3-open-note', { detail: { id: id || dock.activeId() || null } })); return; }
|
|
150
|
-
try { window.open('/', '_blank', 'noopener'); } catch { /* a blocked pop-up */ }
|
|
150
|
+
try { window.open('/classic.html', '_blank', 'noopener'); } catch { /* a blocked pop-up */ }
|
|
151
151
|
};
|
|
152
152
|
|
|
153
153
|
/* The engine and language the live transcript uses, on the chip (notes-recording-reliability): a meeting transcribed in the wrong
|
|
@@ -138,7 +138,7 @@ const importNow = () => {
|
|
|
138
138
|
const notNow = () => run('dismiss', async () => {
|
|
139
139
|
const r = await pmPost('/api/onboarding/dismiss', { ws: S.ws });
|
|
140
140
|
if (!r || !r.ok) { fail(r, 'Not now'); return; }
|
|
141
|
-
location.href = '
|
|
141
|
+
location.href = '/?ws=' + encodeURIComponent(S.ws);
|
|
142
142
|
});
|
|
143
143
|
const undo = () => run('undo', async () => {
|
|
144
144
|
const r = await pmPost('/api/onboarding/undo', { ws: S.ws, import_id: S.importId });
|
|
@@ -284,7 +284,7 @@ function viewBriefing() {
|
|
|
284
284
|
+ (items ? numbered(items) : '<div class="fold" data-testid="no-briefing">Your portfolio is in. Open PMOS to see it.</div>')
|
|
285
285
|
+ (tools.length ? sub('Your tools', 'tools', '', tools.length) + `<ul class="list">${tools.join('')}</ul>` : ''),
|
|
286
286
|
foot(ctl({ variant: 'ghost', html: S.busy === 'undo' ? spin('Undoing') : 'Undo this import', attrs: { 'data-testid': 'undo-link', disabled: !!S.busy, ...busyAttrs('undo') } }),
|
|
287
|
-
ctl({ variant: 'primary', html: 'Open PMOS', href: '
|
|
287
|
+
ctl({ variant: 'primary', html: 'Open PMOS', href: '/?ws=' + encodeURIComponent(S.ws), attrs: { class: 'wl-primary', 'data-testid': 'open-pmos' } }))];
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
const VIEWS = { welcome: viewWelcome, look: viewLook, portfolio: viewPortfolio, questions: viewQuestions, briefing: viewBriefing, sources: viewSources };
|
package/workspace/server.js
CHANGED
|
@@ -5637,12 +5637,14 @@ async function handleRequest(req, res) {
|
|
|
5637
5637
|
const built = BUILD_RE.exec(url.pathname);
|
|
5638
5638
|
const p = built ? built[1] : url.pathname;
|
|
5639
5639
|
|
|
5640
|
-
|
|
5640
|
+
// console3 (public/c3.html) is the default page: '/' and '/index.html' serve it, and '/c3.html' stays an alias so old links
|
|
5641
|
+
// keep working. The older shell (public/index.html) moved to '/classic.html' (c3-default-page), reachable and easy to return to.
|
|
5642
|
+
if (p === '/' || p === '/index.html' || p === '/c3.html' || p === '/classic' || p === '/classic.html' || p === '/welcome' || p === '/welcome.html') {
|
|
5641
5643
|
// pm-os-absorb Run 2 (R25, X11): the per-launch nonce goes into the base meta line, nowhere else. no-store, so a
|
|
5642
5644
|
// restarted server never answers a press from a page that holds the previous launch's value. The page code is
|
|
5643
5645
|
// referenced under this launch's build prefix.
|
|
5644
5646
|
const n = pmGuard.launchNonce();
|
|
5645
|
-
return fs.readFile(path.join(PUB, p === '/
|
|
5647
|
+
return fs.readFile(path.join(PUB, (p === '/classic' || p === '/classic.html') ? 'index.html' : (p === '/welcome' || p === '/welcome.html') ? 'welcome.html' : 'c3.html'), 'utf8', (err, html) => {
|
|
5646
5648
|
if (err) { res.writeHead(404); res.end('not found'); return; }
|
|
5647
5649
|
let out = withBuild(html);
|
|
5648
5650
|
if (n) out = out.replace('<meta name="' + pmGuard.NONCE_META + '" content="" />', '<meta name="' + pmGuard.NONCE_META + '" content="' + n + '" />');
|