Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -497,3 +497,5 @@ Si algo cambia en el proyecto, se actualiza la frase o se añade una nueva, pero
* **2026-11-08** · Fuera *Show Release Notes* del menú Ayuda y de la paleta («quita lo de mostrar notas de la versión»): llevaba a la página de releases del repositorio, y *Show What's New* ya cuenta lo que cambió.
* **2026-11-08** · Las incidencias no recargan al cambiar de panel («cuando cambio entre paneles las issues recargan siempre»): al volver sale al momento la última lista, abierta como estaba; si tiene más de un minuto se comprueba por detrás y solo se repinta si cambió algo. *Actualizar* sigue pidiéndola de verdad.
* **2026-11-08** · Fuera el selector de permisos del chat («Sí, quítalo»): *Ask / Allow all* prometía una elección que no existía, porque pi ejecuta sin preguntar. Los MCP conectados en el propio editor siguen preguntando (ADR-040, adenda). La web y los términos ya dicen que el agente actúa sin preguntar.
* **2026-11-09** · Conectar Jira ya no depende del navegador («no conecta jira», «Sí, arréglalo»): Edge no abría el editor al pulsar *Open PiCode*. Ahora el servidor sigue el intento y el editor le pregunta cómo va, como en GitHub. La página de vuelta enseña a qué cuenta se conecta y pide confirmar, para que nadie pueda llevarse el Jira de otra persona con un enlace (ADR-042, adenda 12).
* **2026-11-09** · Release 2.0.1 en experimental («saca release no? para que probar esto»): lleva la conexión con Jira sin depender del navegador y las builds locales que ya no le quitan los enlaces a la copia instalada.
12 changes: 12 additions & 0 deletions build/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,18 @@ product.dataFolderName = `.${CHANNEL.slug}`;
product.sharedDataFolderName = `.${CHANNEL.slug}-shared`;
product.serverDataFolderName = `.${CHANNEL.slug}-server`;

// A portable build made on this machine, to test a change: it must not pass for the copy installed
// here. With the same name and user model id, Windows treats both as one program and its shortcuts
// and pins end up opening the test build. Releases and installers never get this.
if (!process.env.GITHUB_ACTIONS && process.env.PICODE_SKIP_INSTALLER === 'yes') {
product.picodeLocalBuild = true;
product.nameLong = `${product.nameLong} (local build)`;
product.win32AppUserModelId = `${product.win32AppUserModelId}.LocalBuild`;
product.win32RegValueName = `${product.win32RegValueName}LocalBuild`;
product.win32MutexName = `${product.win32MutexName}-local-build`;
console.log(' local build: its own name and identity, beside the installed copy');
}

// The AppIds are derived, not random: a channel always gets the same
// identifiers, so a rebuild is the same installer, and two channels can sit
// side by side. One hash per key, so x64 and arm64 never share an id.
Expand Down
4 changes: 2 additions & 2 deletions docs/CUENTA.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ El login vive en las tarjetas de *Connections* y las issues se ven en la columna

**Qué ve el usuario**

1. En *Connections*, cada tarjeta (GitHub, Jira) tiene *Connect*. En GitHub, el editor copia un código, abre la página de GitHub y deja el código en la tarjeta por si hace falta de nuevo. Al terminar, si la App de PiCode aún no está instalada en sus repositorios (sin instalar no se ve ninguno), abre sola la página para instalarla y las issues salen en cuanto se instala; si luego falta, el panel lo dice con un botón *Install on GitHub*. En Jira se abre la página de Atlassian y, al aceptar, vuelve solo al editor. Conectada, la tarjeta dice *Connected* (en Jira, a qué sitio) y tiene *Disconnect*. Un intento fallido lo dice en una frase.
1. En *Connections*, cada tarjeta (GitHub, Jira) tiene *Connect*. En GitHub, el editor copia un código, abre la página de GitHub y deja el código en la tarjeta por si hace falta de nuevo. Al terminar, si la App de PiCode aún no está instalada en sus repositorios (sin instalar no se ve ninguno), abre sola la página para instalarla y las issues salen en cuanto se instala; si luego falta, el panel lo dice con un botón *Install on GitHub*. En Jira se abre la página de Atlassian y, al aceptar, una página de PiCode enseña la cuenta a la que se conectará y pide *Connect*; el editor se entera solo, sin depender de que el navegador lo abra. Conectada, la tarjeta dice *Connected* (en Jira, a qué sitio) y tiene *Disconnect*. Un intento fallido lo dice en una frase.
2. En *Status* aparece un panel por servicio conectado (*GitHub issues*, *Jira issues*): las 5 issues abiertas más recientes, con *Show N more*, y *Refresh*. Las del proyecto abierto van primero; en GitHub, hasta 15 repos. Sin conectar, el panel dice *Not connected* y su botón *Connect* abre la ventana Account. Sin plan Pro o sin identificadores registrados, el panel no sale.
3. Pulsar una issue abre el visor (ver abajo). Cada issue ocupa dos líneas: el título entero (hasta dos líneas) y debajo, en pequeño, su etiqueta (si el título empieza por `[algo]`, sale como etiqueta y no come espacio), la clave, la antigüedad y *Outside report* si la escribió alguien ajeno al equipo. A la derecha, *Fix with pi* es un botón solo de icono (el texto sale al pasar el ratón). El título del panel lleva el total de issues abiertas.
4. *Fix with pi* abre un chat nuevo y **escribe** la issue sin enviarla. El usuario la lee y envía. Sin carpeta abierta, con la carpeta sin confianza o con una issue de otro repo, el botón sale apagado con su motivo.
Expand Down Expand Up @@ -103,7 +103,7 @@ Límites: hasta 300 issues por proyecto (lo avisa); sin el permiso de tableros e
| Paneles de Status y *Fix with pi* | `account/issues/picodeStatusIssues.ts`, `picodeIssueActions.ts` |
| Visor (pestaña del editor) | `account/issues/picodeIssuesEditor.ts`, `picodeIssuesEditorInput.ts`, `picodeIssues.contribution.ts` (comando `picode.issues.open`) |
| Lógica pura con pruebas | `picodeConnectorAuth.ts`, `picodeIssueSources.ts`, `picodeIssueBoard.ts`, `picodeIssuePrompt.ts` |
| Servidor (secretos de Atlassian y renovación) | `host/cloud/sync-api`: `POST /api/v1/connect/token`, `POST /api/v1/connect/github`, `GET /api/v1/connect/atlassian/callback` |
| Servidor (secretos de Atlassian y renovación) | `host/cloud/sync-api`: `POST /api/v1/connect/atlassian`, `GET` y `POST /api/v1/connect/atlassian/callback`, `POST /api/v1/connect/github` (y `POST /api/v1/connect/token` para las versiones anteriores a la 2.0.1) |
| Identificadores públicos | `product.json` → `picode.connectors` |

- **GitHub:** GitHub App con inicio por código de un solo uso. Las páginas de login de GitHub no admiten llamadas desde la ventana del editor (sin permiso entre orígenes: «Failed to fetch», visto el 2026-11-06), así que el editor pide el código y la espera por el servidor (`connect/github`), que no guarda nada. Permisos mínimos: *Issues: lectura*. Los repos son los que el usuario eligió al instalarla (`/user/installations`).
Expand Down
2 changes: 2 additions & 0 deletions docs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1842,4 +1842,6 @@ usuario; la App da lectura de issues y solo en los repos que el usuario elija al

**Adenda 10 (2026-11-08) — las issues también en el diseño Editor, y el plan del agente en Status.** *"en la ventana editor no hay nada de las issues de jira y github, quiero… que solo se activa si tienes cuenta pro"*. Una vista nativa del editor (`picodeIssuesView.ts`) pinta el mismo panel dentro del contenedor de PiCode; se une a él cuando el conector lo declara (el contenedor viene de su manifiesto) y se muestra con la clave `picode.account.isPro`. Además, *Status* y *Tasks* enseñan el plan del agente (la herramienta `todo` de gentle-pi), leído del resultado de cada llamada, que trae la lista entera: no se reconstruye nada. La lista queda mientras haya una tarea abierta y 5 minutos después de cerrarse la última. El fichero de estado de gentle-pi 4.7.0 no sirve para esto: solo se escribe cuando pi corre en la terminal.

**Adenda 12 (2026-11-09) — conectar Jira ya no depende del enlace del navegador.** *"no conecta jira"*, *"Sí, arréglalo"*. La vuelta de Atlassian al editor iba por `picode://`: Edge no lo abría al pulsar *Open PiCode* (causa sin averiguar; el mismo enlace lanzado desde Windows sí funciona) y además otra copia de PiCode podía quedarse con el enlace. Ahora el servidor abre un intento para la cuenta, y el editor pregunta cada 2,5 s cómo va, como ya hacía GitHub. **El riesgo nuevo:** si el código se entrega a quien tenga el `state`, alguien podría abrir un intento con su cuenta, mandar el enlace de aprobación a otra persona y recibir el Jira de esa persona. Por eso la página de vuelta enseña a qué cuenta de PiCode se conectará, con la dirección entera y solo si está verificada (una dirección tapada se confunde con otra parecida), y pide confirmar (sin scripts y sin poder meterla en un marco), y el servidor canjea el código en ese momento: nunca se guarda ni llega al editor. **Alternativa descartada:** guardar el código y que lo recogiera el editor, sin confirmación, porque deja abierto ese robo. **Coste:** un clic más. El `state` se guarda solo como hash; los intentos caducan a los 10 minutos y `expireAt` queda listo para una regla TTL de Firestore que borre los abandonados. Las versiones publicadas siguen funcionando con su página y `connect/token`, hasta que se puedan retirar; los `state` del servidor empiezan por `att-`, así que su código nunca sale por `picode://`, ni siquiera con el intento caducado.

**Adenda 11 (2026-11-08) — los tokens de GitHub y Jira pasan al servidor.** *"protégelo anti hacks"*, *"Sí, en el servidor"*. El editor llamaba directo a GitHub y Jira con un token guardado en el equipo, así que cualquier comprobación de Pro se podía quitar parcheando el código. Ahora el servidor guarda los tokens cifrados y hace cada llamada, comprobando el plan cada vez y solo para las llamadas de una lista cerrada. **Alternativa descartada por el dueño:** dejarlos en el equipo, sin credenciales ajenas que custodiar, a costa de que parchear la app permitiera ver las propias issues. **Coste:** responsabilidad sobre credenciales de terceros, más tráfico de servidor y ninguna lectura sin conexión. Se quitan las rutas que devolvían tokens: ninguna release publicada las usaba.
10 changes: 10 additions & 0 deletions docs/howto-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ salida —build bueno, build fallido o `Ctrl-C`—, así que después de compila
limpio. Si el build muere de golpe (sin trampa de salida), la copia sigue ahí y la siguiente
fase 1 la devuelve **antes** de leer nada.

### Un build local no se hace pasar por la copia instalada

Un build portable hecho en tu equipo (`PICODE_SKIP_INSTALLER=yes`, fuera de GitHub Actions) sale
como `PiCode Experimental - … (local build)`, con su propia identidad para Windows. Con la misma,
Windows los trataba como un solo programa y sus accesos terminaban abriendo el build de `D:`.

Tampoco se queda con los enlaces `picode://` si ya los tiene otra copia. Para probar el inicio de
sesión con un build local, arráncalo con `PICODE_CLAIM_LINKS=1`, y abre después la copia instalada
para que los recupere.

## Dependencias en Windows

Los scripts están en **Bash**, así que en Windows se ejecutan desde **Git Bash**
Expand Down
36 changes: 15 additions & 21 deletions extensions/picode/test/picode-connector-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@

import assert from 'assert';
import { test } from 'node:test';
import { atlassianAuthorizeUrl, connectorCodeFromCallback, parseAtlassianSites, parseDeviceCode, parseDevicePoll } from '../../../src/vs/workbench/contrib/picode/browser/account/issues/picodeConnectorAuth.ts';

const callback = (query: string, path = '/connect/callback', scheme = 'picode') => ({ scheme, path, query });
import { atlassianAuthorizeUrl, parseAtlassianSites, parseDeviceCode, parseDevicePoll, readAtlassianAttemptState, readAtlassianAttemptStatus } from '../../../src/vs/workbench/contrib/picode/browser/account/issues/picodeConnectorAuth.ts';

test('a device code from GitHub is read, with its wait in milliseconds', () => {
const code = parseDeviceCode({ device_code: 'd', user_code: 'ABCD-1234', verification_uri: 'https://github.com/login/device', interval: 5, expires_in: 900 });
Expand Down Expand Up @@ -38,26 +36,22 @@ test('a token in the answer is not taken for a connection, nor kept', () => {
assert.deepStrictEqual(parseDevicePoll({ access_token: 'a', refresh_token: 'r' }), { kind: 'failed' });
});

test('the callback of this attempt gives its code', () => {
assert.strictEqual(connectorCodeFromCallback(callback('service=atlassian&state=S1&code=abc'), 'picode', 'atlassian', 'S1'), 'abc');
});

test('the address the cloud page really sends (connect as host, /callback as path) gives its code', () => {
const real = { scheme: 'picode', authority: 'connect', path: '/callback', query: 'service=atlassian&state=S1&code=abc' };
assert.strictEqual(connectorCodeFromCallback(real, 'picode', 'atlassian', 'S1'), 'abc');
assert.strictEqual(connectorCodeFromCallback({ ...real, authority: 'other' }, 'picode', 'atlassian', 'S1'), undefined);
});

test('a callback with another state, service, path or scheme is left alone', () => {
assert.strictEqual(connectorCodeFromCallback(callback('service=atlassian&state=OTHER&code=abc'), 'picode', 'atlassian', 'S1'), undefined);
assert.strictEqual(connectorCodeFromCallback(callback('service=github&state=S1&code=abc'), 'picode', 'atlassian', 'S1'), undefined);
assert.strictEqual(connectorCodeFromCallback(callback('service=atlassian&state=S1&code=abc', '/auth/callback'), 'picode', 'atlassian', 'S1'), undefined);
assert.strictEqual(connectorCodeFromCallback(callback('service=atlassian&state=S1&code=abc', '/connect/callback', 'other'), 'picode', 'atlassian', 'S1'), undefined);
test('the state of a Jira attempt is taken only when it is plain', () => {
assert.strictEqual(readAtlassianAttemptState({ state: '0f8fad5b-d9cb-469f' }), '0f8fad5b-d9cb-469f');
assert.strictEqual(readAtlassianAttemptState({ state: 'short' }), undefined);
assert.strictEqual(readAtlassianAttemptState({ state: 'a&redirect_uri=https://evil' }), undefined);
assert.strictEqual(readAtlassianAttemptState({}), undefined);
assert.strictEqual(readAtlassianAttemptState(undefined), undefined);
});

test('a callback without a code, or with no state expected, is not claimed', () => {
assert.strictEqual(connectorCodeFromCallback(callback('service=atlassian&state=S1'), 'picode', 'atlassian', 'S1'), undefined);
assert.strictEqual(connectorCodeFromCallback(callback('service=atlassian&state=&code=abc'), 'picode', 'atlassian', ''), undefined);
test('a Jira attempt ends only when the server says so', () => {
for (const status of ['connected', 'failed', 'notPro', 'expired'] as const) {
assert.strictEqual(readAtlassianAttemptStatus({ status }), status);
}
assert.strictEqual(readAtlassianAttemptStatus({ status: 'pending' }), 'waiting');
assert.strictEqual(readAtlassianAttemptStatus({ status: 'trading' }), 'waiting');
assert.strictEqual(readAtlassianAttemptStatus({ status: 'other' }), 'waiting');
assert.strictEqual(readAtlassianAttemptStatus(undefined), 'waiting');
});

test('the permission page asks only to read issues, and renews itself', () => {
Expand Down
8 changes: 5 additions & 3 deletions host/cloud/sync-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ test/ # pure rules: plans, retention
| `usage-store.ts` | Los contadores diarios en Firestore (`usage/{día}`) y su lectura para el resumen. |
| `connect.ts` | 🔌 Conectar GitHub y Jira: qué se le pide al proveedor (con el secreto del servidor) y la dirección `picode://` de vuelta. Los tokens nunca vuelven al editor. Sin red ni Next.js. |
| `connect-proxy.ts` | 🔌 La lista cerrada de llamadas a GitHub y Jira que el editor puede pedir. Sin red ni Next.js. |
| `connect-attempts.ts` | 🔌 Cada intento de conectar Jira: la cuenta que lo abrió, cómo va y quién puede preguntarlo. Sin Firestore ni red. |
| `connect-seal.ts` | 🔐 Cifrado de los tokens, con clave propia y ligados a su dueño y servicio. Sin clave no se guarda nada. |
| `connect-store.ts` | 🔌 Las conexiones en `/users/{uid}/connectors/{service}`. |
| `connect-store.ts` | 🔌 Las conexiones en `/users/{uid}/connectors/{service}`, los intentos en `/connectAttempts` y el canje del código de Jira. |
| `connect-renewal.ts` | 🔌 La renovación de los tokens, una a la vez, sin perder una conexión por un fallo pasajero. Sin Firestore ni red. |
| `stats-auth.ts` | Quién puede leer `/api/stats`: solo quien lleve `STATS_TOKEN`; sin token configurado, nadie. |
| `downloads.ts` | Las descargas de la release actual de cada canal, leídas de GitHub. |
Expand All @@ -78,10 +79,11 @@ test/ # pure rules: plans, retention
| `v1/resource/route.ts` | Borra todos los datos del usuario + manifiesto. |
| `v1/collection/` · `v1/collection/[id]/` | Lista, crea y borra colecciones (idempotente). |
| `v1/account/route.ts` | La cuenta, con CORS (la llama el renderer del editor). |
| `v1/connect/token/route.ts` | 🔌 Cambia un código de Atlassian por la conexión y la guarda. Solo Pro (`402`); no devuelve tokens. |
| `v1/connect/atlassian/route.ts` | 🔌 Abre un intento de conectar Jira y dice cómo va. Solo Pro (`402`). |
| `v1/connect/token/route.ts` | 🔌 Para versiones anteriores a los intentos: cambia un código de Atlassian por la conexión y la guarda. Solo Pro (`402`); no devuelve tokens. |
| `v1/connect/proxy/route.ts` | 🔌 Hace una llamada a GitHub o Jira con la conexión guardada. Solo Pro (`402`), en cada llamada; sin Pro, borra las conexiones. |
| `v1/connect/disconnect/route.ts` | 🔌 Borra una conexión. Para cualquier plan. |
| `v1/connect/atlassian/callback/route.ts` | 🔌 A donde vuelve el navegador tras Atlassian: reenvía el código al editor por `picode://connect/callback`. |
| `v1/connect/atlassian/callback/route.ts` | 🔌 A donde vuelve el navegador tras Atlassian: pide confirmar la cuenta y canjea el código. Sin intento abierto (versiones anteriores), lo reenvía al editor por `picode://connect/callback`. |
| `v1/download/` · `v1/download/latest/` | `404` en v1 (no se usa), pero autenticado y con control de plan. |

La tabla completa de métodos y códigos de respuesta está más abajo, en *API endpoints*.
Expand Down
Loading